diff --git a/CHANGELOG.md b/CHANGELOG.md index c7d0d5b65f..675b624f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,13 @@ All notable changes to this project will be documented in this file. provider layer construction/provision, scope teardown, retry behavior, and external-store subscriber observations. +### Changed + +- Port **@overeng/effect-path**, **@overeng/tui-react**, + **@overeng/content-address**, **@overeng/kdl**, **@overeng/kdl-effect**, + **@overeng/effect-distributed-lock**, and **@overeng/otel-contract** to the + Effect 4 beta.102 APIs while preserving their established runtime contracts. + ### Fixed - **@overeng/genie/package-json**: allow consumers to materialize selected diff --git a/context/effect-4/alignment-register.md b/context/effect-4/alignment-register.md index ef93960165..49326bad0c 100644 --- a/context/effect-4/alignment-register.md +++ b/context/effect-4/alignment-register.md @@ -136,6 +136,23 @@ - **Status:** allowlisted as a negative-control diff in `patterns/fork-defaults/scenario.json`. +## effect-path-public-result-shape + +- **Difference:** Public in-memory `Sandbox.validate`, `Sandbox.resolve`, and `validatePath` results + follow Effect's major-version data type: v3 `Either` used `_tag: "Right" | "Left"` with + `right` / `left` fields, while v4 `Result` uses `_tag: "Success" | "Failure"` with + `success` / `failure` fields. +- **Proposed decision:** Accept the v4 `Result` shape. These APIs deliberately expose an + Effect-owned type, and preserving the removed v3 `Either` shape would require a local + compatibility dialect. +- **Blast radius:** `@overeng/effect-path` sandbox validation APIs and consumers that inspect the + returned in-memory Effect data type. +- **Audit evidence:** Repository-wide consumer searches found no downstream call site for these + three APIs and no serialization, snapshot-write, cache, or persistence boundary for their result + objects. Package baselines normalize the branch to its success or failure value. +- **Status:** ACCEPTED for in-memory public API only. Any future serialization site requires a + separate wire-format decision. + ## equality-structural-default - **Difference:** v4 structurally compares plain objects, arrays, maps, sets, diff --git a/context/effect-4/baseline-operations.md b/context/effect-4/baseline-operations.md index f54223de5b..8638c0ad09 100644 --- a/context/effect-4/baseline-operations.md +++ b/context/effect-4/baseline-operations.md @@ -64,6 +64,14 @@ A dev-dependency addition invalidates every lockfile-derived fixed-output deriva them (`flake.nix:199-214`). `evergreen fod chase-fod-closure` cannot help: it requires `passthru.evergreen.fodGraph.v1` metadata the consumers lack. Refresh per attr instead. +Patched dependencies are part of the same authority boundary. Treat `.patch` files as opaque generated +artifacts: put live-migration markers on the wiring that references a patch, never inside its +whitespace-sensitive body. Adding, removing, or regenerating a patch changes the bytes included by the +`oxc-config-plugin` FOD and changes the patch hashes throughout `pnpm-lock.yaml`. Regenerate the +lockfile first, prove every patch still applies, and then refresh all eight attrs. A branch merge that +combines patch changes needs another refresh against the merged graph; updating only the first hash +reported by Nix leaves the remaining boundaries stale. + Package attrs: ```sh diff --git a/context/effect-4/ci-expected-failures.md b/context/effect-4/ci-expected-failures.md new file mode 100644 index 0000000000..3150238052 --- /dev/null +++ b/context/effect-4/ci-expected-failures.md @@ -0,0 +1,146 @@ +# CI expected-failure manifest — Effect 4 integration branch + +Decision **D1** for #925. This file is the gate for the intentionally-red integration branch. + +## The gate + +**CI must run on every push, and the failure set must equal this manifest.** + +Two conditions break it, and the second is the one this file exists for: + +1. a failure appears that is not listed here, or a listed failure starts passing without this file + being updated; +2. **CI does not run at all.** + +Condition 2 is not hypothetical. This branch went **16 commits with no CI whatsoever** because it had +become unmergeable against `main`, which silently suppresses `pull_request` workflows. Nobody noticed, +because the branch was documented as "intentionally red" and slice PRs as "not judgeable by repo CI" — +so *"CI is not running"* and *"CI is red, as expected"* were indistinguishable. A gate that cannot +distinguish **"did not run"** from **"ran and failed"** protects nothing. + +## Baseline + +| | | +|---|---| +| run | **`30548813585`** | +| commit | **`7ef722c3b`** | +| result | 15 failure · 8 success · 2 skipped | +| comparison base | **`main` @ `5f3c78a9f`, run `30461270774` — 25/25 SUCCESS** | + +**`main` is fully green, so there is no base noise to subtract and every failure below is +flip-attributable.** This matters for interpreting the manifest later: "expected" here means +*"caused by the migration and understood"*, never *"we gave up on it"*. + +Do not repeat the epic body's phrase *"the base fails independently on `test-live-deploy-ci-tools`, +`pnpm-builder-contract`, `test-integration-restate` and the FOD checks"* without qualification. It +refers to the **flip base**, not `main`; all four are green on `main`. Read as "main is red", it would +license discarding genuine failures as pre-existing. + +## Green — and two of these are load-bearing + +``` +bootstrap-cold-proof bundle-smoke cargo default-ref-policy +deploy-storybooks source-shape +nix-fod-check (namespace-profile-linux-x86-64) +nix-fod-check (namespace-profile-macos-arm64) +``` + +- **`nix-fod-check` green on both legs** is real evidence the eight-attr FOD refresh on this branch is + correct. That was the expensive open risk. +- **`bootstrap-cold-proof` green** means the genie bootstrap closure still realizes — the deadlock that + bridge B8 exists to hold open has not regressed. + +## Expected failures — attributed + +Cause is the flip: ~52% of the Effect surface is still unported, so anything compiling, linting, +building or testing the workspace fails. + +| job | attribution | +|---|---| +| `typecheck` | unported packages; the measured scope was 6,345 de-duplicated TS errors | +| `lint` | unported source | +| `test (namespace-profile-linux-x86-64)` | unported packages fail to compile/collect | +| `test (namespace-profile-macos-arm64)` | as above | +| `nix-check (namespace-profile-linux-x86-64)` | builds packages from source that does not compile | +| `nix-check (namespace-profile-macos-arm64)` | as above | +| `pnpm-builder-contract` | workspace build of unported packages | +| `pnpm-regression` | as above | +| `test-live-deploy-ci-tools` | `ci-tools` unported; this is the package bridge **B8** pins pre-flip | +| `test-megarepo-cold-gc` | `megarepo` unported (largest slice, 1,388 → 550 errors on its branch) | +| `test-integration-notion` | Notion chain unported (228 Effect files) — confirmed green on `main`, so flip-induced | +| `test-integration-restate` | **designed to fail at the flip.** Baseline #985 pins the v3 parser text inside Restate's HTTP 400 body; v4's `SchemaError(...)` changes it. Mitigation tracked in #978. Do **not** rebaseline. | +| `weaver` | runs an `otel-contract` e2e that imports `@overeng/utils-dev/otelite`; **`utils-dev` is unported** and `src/otelite/Otelite.ts:127` calls `Schema.decodeUnknown`, removed in v4. Clears when `utils-dev` (#1039) lands. See below. | + +### `weaver` — traced, and it needs NO new scope item + +An earlier version of this file called `weaver` an *unscoped port gap* requiring a new Phase 4 entry. +**That was wrong.** Traced properly: + +- `weaver:live-check` (`nix/devenv-modules/tasks/shared/weaver-live-check.nix:29,31`) runs a vitest e2e + in **`packages/@overeng/otel-contract`** — `src/registry-live-check.integration.test.ts` +- that test imports **`@overeng/utils-dev/otelite`** +- **`utils-dev` is unported** on this tip, and `src/otelite/Otelite.ts:127` calls `Schema.decodeUnknown` +- hence `TypeError: Schema.decodeUnknown is not a function` + +So it is an ordinary flip-attributable failure caused by an **unported dependency**, not by `weaver` +and not by `otel-contract`. `Otelite.ts:127` is a blocker #925 already names. It clears when `utils-dev` +(#1039) lands. **Do not add a weaver item to Phase 4.** + +**Checked while here — the enumeration is not structurally incomplete.** Outside +`packages/@overeng/`, the only Effect-importing files on this tip are under `context/` (recipes, docs, +examples); no build lane is missed by the wave lists. + +**Why this correction is itself the useful part:** `otel-contract` is one of the seven packages that +landed by cherry-pick with no CI, and this red says **nothing** about whether `otel-contract` is sound +— it fails on a dependency before reaching it. That is a live instance of the caveat in the last +section, and the reason per-package runs with a non-zero collected count are still owed. + +## UNEXAMINED — must be triaged before they count as expected + +These failed and have **not** been root-caused. They are recorded so they are not silently absorbed. + +- `nix-closure-sizes` +- `devenv-perf` + +**An unexamined entry is indistinguishable from a known one once it is written down.** Both are +plausibly flip-attributable (closure sizes shift when a whole dependency cohort moves; perf lanes +build the workspace), but *plausible* is not *attributed*. Neither may be treated as expected until +triaged, and either could be masking a real regression. + +- [ ] triage `nix-closure-sizes` +- [ ] triage `devenv-perf` + +## Skipped, not failures + +`ci/measurements-report`, `notify-alignment` — conditional jobs. + +## What this manifest does NOT establish + +**It does not verify the seven already-landed packages** (`effect-path`, `tui-react`, +`content-address`, `kdl`, `kdl-effect`, `effect-distributed-lock`, `otel-contract`). Those landed by +cherry-pick with no CI at all, and this run does not close that: + +- `test:` pulls a dependency batch that can fail **before reaching** the package under test — + the normal case while half the graph is unported; +- a red aggregate check says nothing about whether a given package's tests actually **ran**. + +Closing it requires per-package runs: + +```sh +CI=1 devenv tasks run test: --mode single --show-output --no-tui +jq -r '.adapter.records[] | select(._tag=="Metric") | "\(.name)=\(.value)"' +``` + +Read counts from `adapter.records` — there is **no** top-level `vitest.tests`, and a top-level lookup +returns `null`, which reads as "no data" and is one step from being reported as zero. Also read +`child.exit_code` (distinct from the task runner's) and `degraded`. + +**A suite collecting zero tests exits 0 and replays GREEN.** Per #1023 that does not weaken a gate, it +removes it — indistinguishable from passing. Any per-package verification must assert a **non-zero +collected count**. + +## Maintenance + +Update this file in the **same commit** as any change to the failure set, with the reason. As slices +land, entries move from failing to green and are deleted here — **the manifest shrinking is the +progress metric.** When it is empty, the branch is green and D1 is satisfied. diff --git a/context/effect-4/recipes/config-error-collapse.md b/context/effect-4/recipes/config-error-collapse.md new file mode 100644 index 0000000000..1ed03775f8 --- /dev/null +++ b/context/effect-4/recipes/config-error-collapse.md @@ -0,0 +1,36 @@ +# Pattern: config-error-collapse + +**Area:** Config errors **Kind:** conditional + +## Shape change + +Effect 3 exposed `ConfigError` as a union of variants such as `MissingData`, `InvalidData`, `And`, +and `Or`. Effect 4 beta.102 exposes one `Config.ConfigError` class with `_tag: "ConfigError"`. + +## Mapping + +For sites that only carry or catch the error opaquely, replace the v3 module/type with +`Config.ConfigError`: + +```ts +import { Config } from "effect" + +declare const layer: Layer.Layer +``` + +## Mandatory stop condition + +Before applying the import/type mapping, inspect every affected site. If a site discriminates a v3 +variant, reads variant-specific fields, or selectively recovers one variant, **stop**. The variants +have collapsed and there is no faithful mechanical mapping. + +## Verification + +Checked against `effect@4.0.0-beta.102` `Config.ts`. In `@overeng/restate-effect`, all seven affected +sites were type-only or opaque and none discriminated a v3 variant, so the mapping was safe there. + +## Gotchas + +- A green typecheck after broadening handling to the single v4 tag can conceal changed recovery + behavior. The site audit is required. +- Do not invent replacement tags for the removed variants. diff --git a/context/effect-4/recipes/datetime-values.md b/context/effect-4/recipes/datetime-values.md new file mode 100644 index 0000000000..96973b9ab3 --- /dev/null +++ b/context/effect-4/recipes/datetime-values.md @@ -0,0 +1,39 @@ +# Pattern: datetime-values + +**Area:** DateTime values **Kind:** renames **Our usage:** construction and telemetry timestamp +access in `agent-session-ingest`, `otel-contract`, and `utils`. + +## Migration table + +| v3 | v4 | +| ------------------------ | ---------------------------- | +| `DateTime.unsafeMake(x)` | `DateTime.makeUnsafe(x)` | +| `dateTime.epochMillis` | `dateTime.epochMilliseconds` | + +These are value-level `DateTime` APIs. Schema wire codecs are covered by the separate +`schema-date` recipe; do not confuse a value constructor rename with the surviving-name schema +hazard. + +## Equivalence + +Both replacements are **VERIFIED** against the real `effect@3.21.4` and +`effect@4.0.0-beta.102` tarballs. A cross-major probe constructed values from epoch zero, an ISO +timestamp, and a native `Date`. Epoch milliseconds and formatted ISO output matched exactly for +all inputs. + +## Intended differences + +None. + +## Gotchas + +- The `unsafe` word moved; it was not removed. `makeUnsafe` can still throw for invalid input. +- Do not replace `unsafeMake` with safe `make` without handling its result type. +- `Schema.DateTimeUtc` is a different migration: v3's string codec becomes + `Schema.DateTimeUtcFromString`, because bare v4 `Schema.DateTimeUtc` validates an existing + `DateTime.Utc` value. + +## Codemod rule + +Both value-level renames are mechanical. Review every `Schema.DateTime*` occurrence using the +wire-vs-value pattern in `schema-date`. diff --git a/context/effect-4/recipes/duration-input.md b/context/effect-4/recipes/duration-input.md new file mode 100644 index 0000000000..b20823ce1e --- /dev/null +++ b/context/effect-4/recipes/duration-input.md @@ -0,0 +1,41 @@ +# Pattern: duration-input + +**Area:** Duration **Kind:** renames **Our usage:** 10 workspace compiler diagnostics for +`Duration.DurationInput` on the current flip snapshot. + +## Migration table + +| v3 | v4 | +| ------------------------ | --------------------------------- | +| `Duration.DurationInput` | `Duration.Input` | +| `Duration.decode(input)` | `Duration.fromInputUnsafe(input)` | + +V4's `fromInputUnsafe` is the replacement for the total, typed-input v3 `decode`. Keep +`Duration.fromInput` for unknown input that must report invalid data rather than throw. + +## Equivalence + +The aliases, signatures, and replacement are **VERIFIED** against the real `effect@3.21.4` and +`effect@4.0.0-beta.102` tarballs. A cross-major probe compared milliseconds for the repository's +shared input forms: number, bigint nanoseconds, seconds/nanoseconds tuple, duration string, and +infinity. Every result matched. + +V4 `Input` additionally accepts duration objects and explicit infinity strings. That widening does +not change existing v3-valid callers. + +## Intended differences + +None for existing inputs. + +## Gotchas + +- `fromInputUnsafe` is appropriate because the static `Input` type guarantees the accepted shape. + Do not use it at an `unknown` boundary. +- A bigint duration input is nanoseconds in both versions, not milliseconds. +- Do not replace a `Duration.decodeUnknown` boundary mechanically; its validation/result shape + needs separate handling. + +## Codemod rule + +The type and typed constructor renames are mechanical. Unknown-input parsers require per-site +review. diff --git a/context/effect-4/recipes/effect-result.md b/context/effect-4/recipes/effect-result.md new file mode 100644 index 0000000000..1555d44df3 --- /dev/null +++ b/context/effect-4/recipes/effect-result.md @@ -0,0 +1,85 @@ +# Pattern: effect-result + +**Area:** Effect error materialization / Result **Kind:** shape change **Our usage:** 131 reported +`Effect.either` sites; the earlier full-source inventory measured 118 sites across 42 files before +13 additional slice sites were reported. + +## Shape change first + +V3 materialized the typed error channel as `Either`. V4 materializes it as `Result`: + +```ts +// v3 +const either = yield * Effect.either(program) + +if (Either.isRight(either)) { + use(either.right) +} else { + recover(either.left) +} +``` + +```ts +// v4 +const result = yield * Effect.result(program) + +if (Result.isSuccess(result)) { + use(result.success) +} else { + recover(result.failure) +} +``` + +## Migration table + +| v3 | v4 | +| ----------------------- | ------------------------- | +| `Effect.either(effect)` | `Effect.result(effect)` | +| `Either.right(value)` | `Result.succeed(value)` | +| `Either.left(error)` | `Result.fail(error)` | +| `Either.isRight(value)` | `Result.isSuccess(value)` | +| `Either.isLeft(value)` | `Result.isFailure(value)` | +| `either.right` | `result.success` | +| `either.left` | `result.failure` | +| `_tag: "Right"` | `_tag: "Success"` | +| `_tag: "Left"` | `_tag: "Failure"` | +| `Either.mapLeft` | `Result.mapError` | + +Do not stop after renaming `Effect.either`: every downstream guard, match arm, property access, +constructor, serialized tag, and test expectation must move to the `Result` shape. + +## Equivalence + +The API and shape are **VERIFIED** against the real `effect@3.21.4` and +`effect@4.0.0-beta.102` tarballs. A direct probe normalized: + +```text +Effect.succeed(7) -> Success(7) +Effect.fail("bad") -> Failure("bad") +``` + +Both majors produced identical normalized success values and typed failures. In both, the +materialized effect itself cannot fail in the typed error channel. + +Owning slices must compare any observable encoded representation. A raw `JSON.stringify` or +snapshot changes `_tag` and property names even when the normalized value is equivalent. + +## Intended differences + +None at internal control-flow sites. If an `Either` value crosses a wire, persistence, logging, or +snapshot boundary, preserving or intentionally changing its bytes requires a separate alignment +decision. + +## Gotchas + +- `Result` is not `Exit`: a `Result.Failure` contains the typed error directly, not a `Cause`. +- Defects and interruption are not converted into `Result.Failure`; `Effect.result` materializes + the typed error channel, matching v3 `Effect.either`. +- Do not leave `_tag === "Right"` checks or `.right` reads behind; they are runtime shape checks, + not only types. +- Serialized `Either` and `Result` objects are not byte-identical. + +## Codemod rule + +The producer rename is mechanical only together with the complete downstream shape rewrite. +Boundary encoders, snapshots, and pattern matches require per-site review. diff --git a/context/effect-4/recipes/effect-sequencing.md b/context/effect-4/recipes/effect-sequencing.md new file mode 100644 index 0000000000..902c13164d --- /dev/null +++ b/context/effect-4/recipes/effect-sequencing.md @@ -0,0 +1,49 @@ +# Pattern: effect-sequencing + +**Area:** Effect sequencing **Kind:** rename **Our usage:** 70 workspace compiler diagnostics for +`Effect.zipRight` on the current flip snapshot. + +## v3 + +```ts +acquire.pipe(Effect.zipRight(use)) +``` + +## v4 + +```ts +acquire.pipe(Effect.andThen(use)) +``` + +`Effect.zipRight` is absent at beta.102. `Effect.andThen` is its direct replacement when the +right-hand operand is an `Effect`: run the left effect first, discard its success value, then run +and return the right effect. + +## Equivalence + +The replacement is **VERIFIED** against the real `effect@3.21.4` and +`effect@4.0.0-beta.102` tarballs. A direct cross-major probe established that both forms: + +- evaluated the left effect before the right effect; +- returned the right effect's success value; +- did not evaluate the right effect when the left effect failed. + +The declaration signatures also preserve the union of both error channels and both service +requirements. + +## Intended differences + +None. + +## Gotchas + +- V4 `andThen` also accepts plain values and lazy values. Preserve an effectful right-hand operand + as an `Effect`; do not unwrap it into an eager computation. +- This is sequential composition. Do not replace `zipRight` with a concurrent combinator. +- Keep any existing `Effect.uninterruptibleMask`, scope, or transaction boundary around the whole + composition. Moving the boundary between the two effects changes interruption behavior. + +## Codemod rule + +`Effect.zipRight(left, right)` becomes `Effect.andThen(left, right)`, and +`left.pipe(Effect.zipRight(right))` becomes `left.pipe(Effect.andThen(right))`. diff --git a/context/effect-4/recipes/effect-timeout-custom-error.md b/context/effect-4/recipes/effect-timeout-custom-error.md new file mode 100644 index 0000000000..7748f9df2f --- /dev/null +++ b/context/effect-4/recipes/effect-timeout-custom-error.md @@ -0,0 +1,59 @@ +# Pattern: effect-timeout-custom-error + +**Area:** Effect timeout control flow **Kind:** shape change **Our usage:** custom timeout failures +in `megarepo`, `utils`, `utils-dev`, and `restate-effect`. + +## v3 + +```ts +program.pipe( + Effect.timeoutFail({ + duration, + onTimeout: () => new OperationTimeout(), + }), +) +``` + +## v4 + +```ts +program.pipe( + Effect.timeoutOrElse({ + duration, + orElse: () => Effect.fail(new OperationTimeout()), + }), +) +``` + +`timeoutFail` is absent at beta.102. `timeoutOrElse` is the replacement, but its fallback is an +`Effect`, not a raw error value. State the `Effect.fail` explicitly; replacing the removed API with +plain `timeout` would change the error type to `TimeoutException`. + +## Equivalence + +The replacement is **VERIFIED** against the real `effect@3.21.4` and +`effect@4.0.0-beta.102` tarballs. A direct probe covered both branches: + +- an immediately successful source returned the same value and did not evaluate the timeout error; +- a never-completing source timed out into the same normalized typed error; +- the lazy timeout constructor ran exactly once, only on the timeout branch. + +Both implementations interrupt the source before producing the timeout failure. + +## Intended differences + +None. + +## Gotchas + +- Preserve laziness: `orElse: () => Effect.fail(onTimeout())`, not + `orElse: Effect.fail(onTimeout())`. +- Use `Effect.fail`, not `Effect.die`, unless the v3 site used `timeoutFailCause`. +- Keep the original duration input and custom error payload unchanged. +- Test both branches. A timeout-only test does not prove the success path avoids constructing the + error. + +## Codemod rule + +The outer call shape is predictable, but the error must be lifted into `Effect.fail`. Review +`timeoutFailCause` separately because its v3 failure is a Cause/defect boundary. diff --git a/context/effect-4/recipes/filesystem-watch-ordering.md b/context/effect-4/recipes/filesystem-watch-ordering.md index 051082f43b..3bc17fa177 100644 --- a/context/effect-4/recipes/filesystem-watch-ordering.md +++ b/context/effect-4/recipes/filesystem-watch-ordering.md @@ -16,7 +16,7 @@ const wholeTree = fs.watch(root, { recursive: true }) ## v4 ```ts -import { FileSystem } from 'effect' +import { FileSystem } from 'effect/FileSystem' const fs = yield * FileSystem.FileSystem const wholeTree = fs.watch(root) diff --git a/context/effect-4/recipes/inspectable-redaction.md b/context/effect-4/recipes/inspectable-redaction.md new file mode 100644 index 0000000000..7a28644024 --- /dev/null +++ b/context/effect-4/recipes/inspectable-redaction.md @@ -0,0 +1,38 @@ +# Pattern: inspectable-redaction + +**Area:** Logging / inspection / secrets **Kind:** rename and module move **Our usage:** file logger +JSON rendering and redaction. + +## Migration table + +| v3 | v4 | +| ----------------------- | ----------------------- | +| `Inspectable.toJSON(x)` | `Inspectable.toJson(x)` | +| `Inspectable.redact(x)` | `Redactable.redact(x)` | + +Import `Redactable` from the `effect` root for the second replacement. Redaction was moved, not +removed; dropping the call can expose secret values in logs. + +## Equivalence + +The APIs and behavior are **VERIFIED** against the real `effect@3.21.4` and +`effect@4.0.0-beta.102` tarballs. A cross-major probe used an object with a custom `toJSON`, a +nested array, and a `Redacted` secret. Both `toJSON`/`toJson` and both redaction paths produced the +same recursive JSON shape with the secret rendered as `""`. + +## Intended differences + +None. + +## Gotchas + +- `toJson` still invokes user-defined `toJSON()` methods; only the exported helper's capitalization + changed. +- `Redactable.redact` returns `unknown`. Preserve the old boundary's validation or serialization + after redaction. +- Never replace `Inspectable.redact` with identity or raw `JSON.stringify`. + +## Codemod rule + +The helper rename is mechanical. The module move additionally requires adding the `Redactable` +root import and preserving redaction before serialization. diff --git a/context/effect-4/recipes/layer-clock-providers.md b/context/effect-4/recipes/layer-clock-providers.md index 4911eda7b6..18b1bebaba 100644 --- a/context/effect-4/recipes/layer-clock-providers.md +++ b/context/effect-4/recipes/layer-clock-providers.md @@ -87,7 +87,9 @@ const deterministicRandom = Layer.succeed(Random.Random, { ``` Callers continue to use effectful operations such as `Random.next`, `Random.nextInt`, and -`Random.shuffle`; those operations derive from the two Reference methods. +`Random.shuffle`; those operations derive from the two Reference methods. Effect 3 call sites of +`Random.nextIntBetween(min, max)` must pass `{ halfOpen: true }` in v4 to preserve the exclusive +upper bound; see `random-bounds-semantics.md`. ## Custom Clock @@ -186,6 +188,8 @@ documents the constructor replacements covered here. - A v3 custom Random object cannot be installed unchanged. Implement the v4 Reference's `nextIntUnsafe` and `nextDoubleUnsafe` primitives and prove the derived sequence used by the slice. +- The v4 default for `Random.nextIntBetween` includes the upper bound. Preserve v3 semantics with + `{ halfOpen: true }`; a mid-range-only test does not detect this breakage. - `ConfigProvider.fromUnknown` treats empty strings as missing unless `{ preserveEmptyStrings: true }` is passed. diff --git a/context/effect-4/recipes/layer-memoization.md b/context/effect-4/recipes/layer-memoization.md index 568f85b2a1..ab13b6da6f 100644 --- a/context/effect-4/recipes/layer-memoization.md +++ b/context/effect-4/recipes/layer-memoization.md @@ -23,6 +23,19 @@ program.pipe(Effect.provide(Layer.fresh(Live)), Effect.provide(Live)) // acquire runs twice ``` +## Explicit `Layer.memoize` ports + +Deleting `Layer.memoize` is not a faithful port. Effect 4 does not memoize across separate builds: +two `Layer.buildWithScope` calls on one layer measured `acquisitions=2, same=false`. Deleting the +wrapper silently doubles every acquisition behind that layer. + +To restore v3 sharing, thread one `Layer.makeMemoMap` through `Layer.buildWithMemoMap`. The equivalent +probe measured `acquisitions=1` with an identical service id. + +Apply the shared map only where v3 shared. Independence is preserved by default: non-primary paths +measured `acquisitions=2, same=false, ids 1,2` in both majors. Threading one map too widely collapses +deployments that were deliberately isolated. + ## Equivalence Command: @@ -32,20 +45,24 @@ bun run run layer-memoization ``` Result: default separate provides differ (`v3: 2`, `v4: 1`) and are allowlisted -as a semantic change. `Layer.fresh` preserves v3 construction count. v4 -`{ local: true }` is available and also restores the v3 count when applied to the -inner provide. +as a semantic change. In this separate-provide probe, `Layer.fresh` preserves v3 +construction count. v4 `{ local: true }` is available and also restores the v3 +count when applied to the inner provide. ## Intended differences (alignment register entries) - v4 shared memoization is accepted for normal application wiring, but tests and resource factories that require fresh state must use `Layer.fresh` or `{ local: true }`. +- Explicit v3 `Layer.memoize` sites that require sharing across builds must use + one `Layer.makeMemoMap` with `Layer.buildWithMemoMap`. ## Gotchas - A careless migration can silently reduce resource construction from two to one. That is usually good for live resources but wrong for tests expecting isolated state. +- `Layer.fresh` is not the replacement for `Layer.memoize`: it has inverted + semantics. `fresh` defeats memoization; `memoize` enabled it. - The placement of `{ local: true }` matters. In this probe, the inner provide is the placement that restores the v3 construction count. diff --git a/context/effect-4/recipes/metric-updates-attributes.md b/context/effect-4/recipes/metric-updates-attributes.md new file mode 100644 index 0000000000..4654fd144a --- /dev/null +++ b/context/effect-4/recipes/metric-updates-attributes.md @@ -0,0 +1,131 @@ +# Pattern: metric-updates-attributes + +**Area:** Metrics / telemetry **Kind:** shape change **Our usage:** about 29 affected references, +including the schema-backed instruments in `otel-contract`, resource gauges in `utils`, and +telemetry tests. + +## Shape changes first + +- A v4 `Metric` is no longer a callable effect. Mutations go through `Metric.update`. +- `MetricLabel` is removed. V4 metrics use string attributes supplied as a record or + `ReadonlyArray<[string, string]>`. +- Attribute names must be unique for a faithful port. V3 could retain two `MetricLabel`s with the + same key; v4 attributes collapse them to one key. +- Snapshot entries changed from `{ metricKey, metricState }` to + `{ id, type, state, attributes }`. + +All mappings and observations below are **VERIFIED** against the real +`effect@4.0.0-beta.102` tarball (SHA-1 +`f51092854960f60cbdb06bd59e788acbc8ee8492`). + +## v3 + +```ts +import { Metric, MetricLabel } from 'effect' + +const labels = [MetricLabel.make('route', 'alpha'), MetricLabel.make('method', 'GET')] + +const counter = Metric.counter('requests').pipe(Metric.taggedWithLabels(labels)) +const gauge = Metric.gauge('queue_depth') + +yield * Metric.increment(counter) +yield * Metric.incrementBy(counter, 2) +yield * Metric.set(gauge, 42) +``` + +## v4 + +```ts +import { Metric } from 'effect' + +const attributes: Metric.Metric.Attributes = [ + ['route', 'alpha'], + ['method', 'GET'], +] + +const counter = Metric.counter('requests').pipe(Metric.withAttributes(attributes)) +const gauge = Metric.gauge('queue_depth') + +yield * Metric.update(counter, 1) +yield * Metric.update(counter, 2) +yield * Metric.update(gauge, 42) +``` + +## Migration table + +| v3 | v4 | Shape | +| ----------------------------------------- | ------------------------------------------------------------------------ | ------------------------------ | +| `Metric.increment(metric)` | `Metric.update(metric, 1)` | named mutation to input | +| `Metric.incrementBy(metric, amount)` | `Metric.update(metric, amount)` | named mutation to input | +| `Metric.set(gauge, value)` | `Metric.update(gauge, value)` | named mutation to input | +| `Metric.tagged(metric, key, value)` | `Metric.withAttributes(metric, [[key, value]])` | label to attribute | +| `Metric.taggedWithLabels(metric, labels)` | `Metric.withAttributes(metric, attributes)` | label objects to attribute set | +| `MetricLabel.make(key, value)` | `[key, value] as const` | object to tuple | +| `ReadonlyArray` | `Metric.Metric.Attributes` or `ReadonlyArray` | nominal objects to strings | +| `pair.metricKey.name` | `snapshot.id` | snapshot shape | +| `pair.metricKey.tags` | `snapshot.attributes` | array to record | +| `pair.metricState` | `snapshot.state` | field rename | + +Use the tuple form when keys are computed. A record is convenient for known static keys: + +```ts +Metric.withAttributes(metric, { route: 'alpha', method: 'GET' }) +``` + +## Equivalence + +A direct cross-major probe used the real `effect@3.21.4` and +`effect@4.0.0-beta.102` tarballs. After normalizing the snapshot shapes: + +- v3 `increment` followed by `incrementBy(2)` and v4 `update(1)` followed by `update(2)` both + produced counter value `3`; +- a second label set produced a separate counter series with value `1` in both majors; +- v3 `set(41)` followed by `increment` and v4 `update(41)` followed by `update(42)` both produced + gauge value `42`; +- both snapshots contained exactly three series with the same unique-key label/attribute sets and + values. + +This verifies the replacement for the repository's ordinary unique-key telemetry. Each owning +slice must still compare exported instrument name, kind, description, unit, attribute set, value, +and series cardinality through its real telemetry exporter. + +## Duplicate-key trap + +The same probe deliberately composed two values for one key: + +```ts +// v3 snapshot labels: [["key", "a"], ["key", "b"]] +metric.pipe(Metric.tagged('key', 'a'), Metric.tagged('key', 'b')) + +// v4 snapshot attributes: { key: "a" } +metric.pipe(Metric.withAttributes({ key: 'a' }), Metric.withAttributes({ key: 'b' })) +``` + +V4 collapses duplicate keys, and the tested composition retained the earlier value. Do not port a +site with duplicate or potentially colliding keys until its intended cardinality and precedence +are explicit. Schema/object-field-derived labels with unique field names can move directly to +attributes. + +## Intended differences + +None for unique-key metrics. Counter totals, gauge values, instrument metadata, attributes, and +series cardinality must be preserved. + +Duplicate-key labels have no equivalent representation in v4 attributes. No current migration +decision authorizes silently collapsing them. + +## Gotchas + +- `Metric.update` means “apply the metric's input”, not always “replace”. It adds to counters, + replaces gauges, records histogram observations, and increments frequency occurrences. +- Do not replace `incrementBy(metric, amount)` with `update(metric, 1)`. +- Do not discard labels merely because `MetricLabel` is absent. The replacement is attributes. +- Do not compare only an in-process value. Metrics are telemetry: verify exporter-visible metadata, + attributes, values, and series count. +- Tuple attributes are converted to a string-keyed attribute set. Validate uniqueness before + conversion when keys are dynamic. + +## Codemod rule + +The three mutation replacements are mechanical after confirming the metric kind. Label migration +requires changing the producer and consumer types together and reviewing duplicate-key behavior. diff --git a/context/effect-4/recipes/option-nullish.md b/context/effect-4/recipes/option-nullish.md new file mode 100644 index 0000000000..8630039993 --- /dev/null +++ b/context/effect-4/recipes/option-nullish.md @@ -0,0 +1,40 @@ +# Pattern: option-nullish + +**Area:** Option constructors **Kind:** mechanical rename **Our usage:** 19 references across 15 +files and six packages. + +## v3 + +```ts +Option.fromNullable(value) +``` + +## v4 + +```ts +Option.fromNullishOr(value) +``` + +This replacement is **VERIFIED** against the real `effect@4.0.0-beta.102` tarball: +`fromNullable` is absent and `fromNullishOr` is present. + +## Equivalence + +Both constructors map `null` and `undefined` to `Option.none()` and every other value to +`Option.some(value)`. + +Do not substitute the narrower `fromNullOr` or `fromUndefinedOr`: + +| Input | `fromNullishOr` | `fromNullOr` | `fromUndefinedOr` | +| ----------- | --------------- | ----------------- | ----------------- | +| `null` | `None` | `None` | `Some(null)` | +| `undefined` | `None` | `Some(undefined)` | `None` | + +## Intended differences + +None. + +## Codemod rule + +`Option.fromNullable` mechanically becomes `Option.fromNullishOr`. Preserve any explicit fallback +argument or surrounding `Option` combinators unchanged. diff --git a/context/effect-4/recipes/platform-filesystem.md b/context/effect-4/recipes/platform-filesystem.md index 884c1cf637..4bc90685be 100644 --- a/context/effect-4/recipes/platform-filesystem.md +++ b/context/effect-4/recipes/platform-filesystem.md @@ -18,7 +18,8 @@ const program = effect.pipe(Effect.provide(NodeContext.layer)) ## v4 ```ts -import { FileSystem, Path } from 'effect' +import { FileSystem } from 'effect/FileSystem' +import { Path } from 'effect/Path' import { NodeServices } from '@effect/platform-node' const fs = yield * FileSystem.FileSystem diff --git a/context/effect-4/recipes/predicate-object.md b/context/effect-4/recipes/predicate-object.md new file mode 100644 index 0000000000..a48695dbd7 --- /dev/null +++ b/context/effect-4/recipes/predicate-object.md @@ -0,0 +1,42 @@ +# Pattern: predicate-object + +**Area:** Runtime predicates **Kind:** semantic rename **Our usage:** object-or-function guards in +shared utilities. + +## v3 + +```ts +Predicate.isObject(value) +``` + +## v4 replacement + +```ts +Predicate.isObjectKeyword(value) +``` + +V4 still exports `Predicate.isObject`, but its meaning narrowed to non-null records: it rejects +arrays and functions. Use `isObjectKeyword` to preserve v3's JavaScript `object`-keyword semantics, +which accept non-null objects, arrays, and functions. + +## Equivalence + +The predicates are **VERIFIED** against the real `effect@3.21.4` and +`effect@4.0.0-beta.102` tarballs. Across `null`, plain object, array, function, and string, v3 +`isObject` and v4 `isObjectKeyword` returned identical results. V4 `isObject` differed for arrays +and functions. + +## Intended differences + +None when using `isObjectKeyword`. + +## Gotchas + +- This is a surviving-name hazard: leaving `isObject` unchanged compiles but changes behavior. +- If a site deliberately wants record-only semantics, v4 `isObject` is appropriate, but that is a + behavior change requiring an explicit decision. + +## Codemod rule + +Replace v3 `Predicate.isObject` with v4 `Predicate.isObjectKeyword` unless the owning slice +explicitly adopts record-only semantics and tests arrays and functions. diff --git a/context/effect-4/recipes/random-bounds-semantics.md b/context/effect-4/recipes/random-bounds-semantics.md new file mode 100644 index 0000000000..5d46fbdfab --- /dev/null +++ b/context/effect-4/recipes/random-bounds-semantics.md @@ -0,0 +1,79 @@ +# Pattern: random-bounds-semantics + +**Area:** Random integer bounds **Kind:** semantic **Our usage:** `restate-effect` uses +`Random.nextIntBetween` for journaled deterministic values in runtime tests, examples, and the +determinism guide. + +## Shape change first + +Effect 3's `Random.nextIntBetween(min, max)` is half-open: it can return `min` but never `max`. +Effect 4 beta.102 makes the upper bound inclusive by default and adds an explicit option for the +old behavior. + +| operation | lower bound | upper bound | +| --- | --- | --- | +| v3 `Random.nextIntBetween(min, max)` | inclusive | exclusive | +| v4 default `Random.nextIntBetween(min, max)` | inclusive | inclusive | +| v4 `Random.nextIntBetween(min, max, { halfOpen: true })` | inclusive | exclusive | + +## v3 + +```ts +const index = yield* Random.nextIntBetween(0, values.length) +``` + +## v4 + +```ts +const index = yield* Random.nextIntBetween(0, values.length, { halfOpen: true }) +``` + +Do not omit the option when porting a v3 call. An inclusive result equal to `values.length` can +turn a previously valid array index into `undefined`. + +## Custom Random services + +Effect 4's `Random.Random` Reference exposes only `nextIntUnsafe` and `nextDoubleUnsafe`; the +effectful operations derive from those primitives. When a v3 custom Random supplied its own +integer derivation, preserve that local arithmetic in `nextIntUnsafe` instead of substituting the +v4 default generator's full-safe-integer behavior. + +For Restate's journaled service, the v3 implementation was: + +```ts +const next = Effect.sync(() => ctx.rand.random()) +const nextInt = Effect.map(next, (n) => Math.floor(n * Number.MAX_SAFE_INTEGER)) +``` + +The equivalent v4 primitive service is: + +```ts +const journaledRandom: (typeof Random.Random)["Service"] = { + nextDoubleUnsafe: () => ctx.rand.random(), + nextIntUnsafe: () => Math.floor(ctx.rand.random() * Number.MAX_SAFE_INTEGER), +} +``` + +This custom-service mapping is specific to that source-derived v3 implementation. It is not a +universal replacement for `Random.make`. + +## Equivalence + +The beta.102 implementation computes `extra = options?.halfOpen === true ? 0 : 1` before flooring +the scaled random value. A discriminating source value of `0.999` over `(0, 10)` returns `9` with +`{ halfOpen: true }` and can return `10` with the v4 default. Preserve the edge case in a runtime +test; mid-range values such as `0.42` do not distinguish the two semantics. + +The API and arithmetic were verified directly against the +`effect@4.0.0-beta.102` tarball (SHA-1 `f51092854960f60cbdb06bd59e788acbc8ee8492`). + +## Intended differences + +None. The inclusive default is real breakage for v3 call sites and must be preserved with +`{ halfOpen: true }`. + +## Codemod rule + +Repository-owned v3 call sites can add `{ halfOpen: true }` mechanically after confirming they +target Effect's `Random.nextIntBetween`. Custom Random implementations require a source-derived +port of their primitive arithmetic. diff --git a/context/effect-4/recipes/root-import-renames.md b/context/effect-4/recipes/root-import-renames.md new file mode 100644 index 0000000000..e852279f10 --- /dev/null +++ b/context/effect-4/recipes/root-import-renames.md @@ -0,0 +1,193 @@ +# Pattern: root import renames + +**Area:** Effect root exports / schema parsing / metrics / loader API moves **Kind:** mechanical +root export move with call-shape changes +**Our usage:** `otel-contract` reached all three removed root exports during the flip branch module +load path. The same mappings may clear other current-source tool builds whose Nix/runtime closure +imports `otel-contract`. + +The flip branch also rewrote imports from old consolidated packages (`@effect/cli`, +`@effect/platform`) into the `effect` root. Treat those as **Category A rewrite fallout**, not +removed root exports. File count observed in the branch handoff: 165 files out of the 309 changed by +the flip. Do not encode same-named platform symbols as Effect root API changes when beta.102 still +exports them from the root. + +## v3 + +```ts +import { Either, MetricBoundaries, ParseResult } from 'effect' +``` + +## v4 + +```ts +import { Metric, Result, SchemaIssue, SchemaParser, SchemaTransformation } from 'effect' +``` + +The exact mappings for `effect@4.0.0-beta.102` are: + +```text +Either -> Result +ParseResult -> SchemaIssue + SchemaParser +MetricBoundaries -> Metric.boundariesFromIterable / Metric.linearBoundaries / Metric.exponentialBoundaries +``` + +## Category A: bad consolidated-package root rewrite + +These came from package moves, not from root removals: + +| v3 package/source | v4 package/source | Notes | +| ---------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `@effect/cli/Args` | `effect/unstable/cli` `Argument` | Namespace renamed. | +| `@effect/cli/Options` | `effect/unstable/cli` `Flag` | `text()` became `string()`. | +| `@effect/cli/Command` | `effect/unstable/cli` `Command` | Do not import from root. | +| `@effect/cli` namespace | `effect/unstable/cli` namespace | Use `Cli.Argument` / `Cli.Flag`. | +| `@effect/platform/HttpClient*` | `effect/unstable/http` | `HttpClient`, `HttpClientRequest`, `HttpClientResponse`, `HttpClientError`, `FetchHttpClient`. | +| `@effect/platform/Command` | `effect/unstable/process` `ChildProcess` | Existing local alias can preserve call-site name. | +| `@effect/platform/CommandExecutor` | `effect/unstable/process` `ChildProcessSpawner` | Service type is `ChildProcessSpawner.ChildProcessSpawner`. | + +`FileSystem`, `Path`, and `PlatformError` are present in beta.102 root exports. If they appear in a +Category A provenance scan, classify them as rewrite audit hazards rather than genuine root removals. + +## Mapping details + +### Either -> Result + +`Result` is the beta.102 data-success/data-failure successor. It is still a root export, but its +constructors, guards, and property names are not the old `Either` spellings: + +```ts +Either.right(value) -> Result.succeed(value) +Either.left(error) -> Result.fail(error) +Either.isRight(result) -> Result.isSuccess(result) +Either.mapLeft(result, f) -> Result.mapError(result, f) +either.right -> result.success +either.left -> result.failure +``` + +## ParseResult split + +`ParseResult` split into two namespaces: + +- `SchemaParser` owns parse functions such as `decodeUnknownEffect`, `decodeUnknownResult`, and + `decodeResult`. +- `SchemaIssue` owns issue/error values such as `InvalidValue`, `Forbidden`, `Encoding`, and + `defaultFormatter`. + +Do not map every `ParseResult.*` member to one namespace. Check each member at the call site. + +Effectful transformations also moved shape. The old `Schema.transformOrFail(from, to, options)` call +site becomes a schema link: + +```ts +From.pipe( + Schema.decodeTo( + To, + SchemaTransformation.transformOrFail({ + decode: (encoded) => SchemaParser.decodeUnknownEffect(To)(encoded), + encode: (decoded) => Effect.fail(new SchemaIssue.Forbidden(Option.some(decoded), undefined)), + }), + ), +) +``` + +## MetricBoundaries trap + +`MetricBoundaries` was a namespace in v3. In beta.102 it is not a root export, and this is not only +an import-specifier rewrite. The replacement is functions on `Metric`, and metric constructors also +take an options object: + +```ts +Metric.histogram(name, MetricBoundaries.fromIterable(boundaries), description) +``` + +becomes: + +```ts +Metric.histogram(name, { + boundaries: Metric.boundariesFromIterable(boundaries), + description, +}) +``` + +Linear and exponential generated buckets become: + +```ts +Metric.linearBoundaries({ start, width, count }) +Metric.exponentialBoundaries({ start, factor, count }) +``` + +This trap can pass type-oriented greps and still fail at runtime because the old namespace no longer +exists and the histogram call shape changed. + +## Intended differences + +None. These are beta.102 API moves. Preserve existing decoding, encoding, and metric boundary +semantics unless a separate slice records and proves a behavior change. + +## Gotchas + +- `Result` uses `_tag: "Success" | "Failure"` and `.success` / `.failure`, not `Right` / `Left`. +- `Schema.decodeUnknownResult` wraps parser issues as `Schema.SchemaError`; use `SchemaParser` when + the transformation itself must fail with `SchemaIssue.Issue`. +- `Metric.boundariesFromIterable` is the direct replacement for the old explicit-boundaries helper. + Use `linearBoundaries` and `exponentialBoundaries` only for generated bucket sequences. +- This mapping explains the earlier census result where `ci-tools` was red because its shell + realization reached `otel-contract`. +- `@effect/atom-react` is not currently installed in the branch. A full reinstall / `pnpm:repair` + removes any temporary manual materialization and makes `genie:run` fail there before later loader + blockers. + +## Loader API moves observed after the root-export repairs + +These were verified against `effect@4.0.0-beta.102` while chasing `genie:run` module-load blockers. + +| v3 shape | beta.102 shape | Notes | +| ---------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| `Schema.NonEmptyTrimmedString` | `Schema.Trimmed.check(Schema.isNonEmpty())` | Value helper removed. | +| `Schema.NonNegativeInt` | `Schema.Natural` | `Schema.Int` still exists; `Natural` is `Int >= 0`. | +| `Schema.NonNegativeBigInt` | `Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n))` | No `NaturalBigInt` export observed. | +| `Schema.TaggedError` | `Schema.TaggedErrorClass` | Same class-style call family. | +| `Schema.Defect` | `Schema.Defect()` | Became a schema factory function. | +| `Schema.parseJson(schema)` | `Schema.fromJsonString(schema)` | Zero-arg form becomes `Schema.fromJsonString(Schema.Unknown)`. Pretty-print options are not carried by this helper. | +| `Schema.maxLength(n)` / `Schema.minLength(n)` | `Schema.check(Schema.isMaxLength(n))` / `Schema.check(Schema.isMinLength(n))` | Filters are not pipe functions by themselves. | +| `Schema.isPattern(re)` in `.pipe(...)` | `Schema.check(Schema.isPattern(re))` | Direct `.check(Schema.isPattern(re))` is already valid. | +| `Schema.Union(A, B)` | `Schema.Union([A, B])` | Existing array calls stay unchanged. | +| `Schema.Tuple(A, B)` | `Schema.Tuple([A, B])` | Existing array calls stay unchanged. | +| `Schema.fromBrand(ctor)(schema)` | `Schema.fromBrand(identifier, ctor)(schema)` | Bare constructor overload removed. | +| `Schema.transform(from, to, options)` | `from.pipe(Schema.decodeTo(to, options))` | For effectful transforms use `SchemaTransformation.transformOrFail(...)`. | +| `SchemaAST.getAnnotation(ast, stringKey)` | `SchemaAST.resolveAt(stringKey)(ast)` | String keys only; returns `T | undefined`, not `Option`. | +| `SchemaAST.getAnnotation(symbolKey)(ast)` | `SchemaAST.resolve(ast)?.[symbolKey] as T \| undefined` | `resolveAt` does not accept symbols. | +| `Context.Tag("id")()` | `Context.Service()("id")` | Class-style service tags. | +| `Logger.prettyLogger()` | `Logger.consolePretty()` | Console pretty logger constructor. | +| `Logger.replace(Logger.defaultLogger, logger)` | `Logger.layer([logger])` | To keep default plus custom logger use `Logger.layer([Logger.defaultLogger, logger])`. | + +Known remaining source search after this batch: `Logger.replaceScoped` in browser broadcast logging +was not on the `genie:run` loader path and was not ported in this slice. + +## SchemaAST annotation-key split + +`SchemaAST.resolveAt` is only the replacement for **string-keyed** annotations. Beta.102 declares +its key parameter as `string`; passing a `unique symbol` is not a valid migration. + +For symbol-keyed annotations, resolve the annotation object and index it with the original symbol: + +```ts +const annotated = SchemaAST.resolve(schema.ast)?.[optionValueSchema] as + | Schema.Codec + | undefined + +if (annotated !== undefined) { + use(annotated) +} +``` + +This is **VERIFIED** against the real beta.102 runtime: a schema annotated under a unique symbol was +recovered by `SchemaAST.resolve(ast)?.[symbol]`, while `resolveAt(symbol.description)` returned +`undefined`. `resolve` follows beta.102's checked-schema rule by reading the last check's +annotations when checks are present. + +The cast is required because beta.102's public `Annotations.Annotations` interface has only a +string index signature even though the runtime preserves symbol properties. Remove v3 +`Option.isSome`, `.value`, and `Option.getOrUndefined` handling; both `resolveAt` and direct symbol +indexing use `undefined` for absence. diff --git a/context/effect-4/recipes/schedule-composition-metadata.md b/context/effect-4/recipes/schedule-composition-metadata.md new file mode 100644 index 0000000000..e12cebf26c --- /dev/null +++ b/context/effect-4/recipes/schedule-composition-metadata.md @@ -0,0 +1,90 @@ +# Pattern: schedule-composition-metadata + +**Area:** Retry/repeat scheduling **Kind:** shape change **Our usage:** `megarepo` git retries and +retry metadata logging, plus schedule composition in Notion and release packages. + +## Shape changes first + +- V3 `Schedule.intersect(A, B)` continued only while both schedules recurred, waited for the longer + delay, and output `[AOutput, BOutput]`. +- V4 `Schedule.max([A, B])` preserves the continuation and maximum-delay behavior but outputs the + selected `Duration`, not the tuple. +- `Schedule.CurrentIterationMetadata` becomes `Schedule.CurrentMetadata`. +- Metadata `recurrence` becomes `attempt`. +- V3 `elapsed` / `elapsedSincePrevious` were `Duration`; v4 stores their millisecond values as + `number`. + +## Git retry replacement + +The repository's git retry does not consume the composed schedule output, so its faithful +replacement is: + +```ts +// v3 +const retry = Schedule.exponential('2 seconds').pipe( + Schedule.intersect(Schedule.recurs(GIT_MAX_RETRIES)), +) + +// v4 +const retry = Schedule.max([Schedule.exponential('2 seconds'), Schedule.recurs(GIT_MAX_RETRIES)]) +``` + +Metadata logging becomes: + +```ts +const metadata = yield * Schedule.CurrentMetadata + +if (metadata.attempt > 0) { + yield * + Effect.logWarning('Retrying').pipe( + Effect.annotateLogs({ + attempt: metadata.attempt, + elapsed: Duration.format(Duration.millis(metadata.elapsed)), + error: String(metadata.input), + }), + ) +} +``` + +## Equivalence + +All APIs and metadata shapes are **VERIFIED** against the real beta.102 tarball. A direct +cross-major retry probe used exponential delay plus a three-retry cap. Both traces were: + +```text +attempt 0, input absent +attempt 1, input first failure +attempt 2, input second failure +attempt 3, input third failure +success on the fourth execution +``` + +This establishes the replacement for the repository's output-discarding retry schedule. Owning +slices must additionally assert their real delay sequence, retry cap, `while` classification, and +logged metadata. + +## Output-shape trap + +`Schedule.max` is not a complete replacement when the v3 tuple output is consumed. It emits only a +`Duration`. Such sites need an explicit `Schedule.fromStep` composition or a redesigned output +projection, with a behavior probe. Do not cast the duration to the old tuple or silently discard an +output used by retry logic. + +## Intended differences + +None for the git retry continuation, delay, cap, and metadata behavior. The schedule's unused output +shape may narrow to `Duration`; consumed tuple outputs require separate adjudication. + +## Gotchas + +- `Schedule.min` has the opposite continuation/delay semantics: it continues while any schedule + recurs and picks the fastest delay. It is not the replacement for v3 `intersect`. +- `metadata.elapsed` is a number of milliseconds; wrapping it with `Duration.millis` preserves + v3 `Duration.format` call sites. +- Retry attempt zero has no previous failure input. Do not read/cast `metadata.input` before + checking `attempt > 0`. + +## Codemod rule + +Only output-discarding `intersect` sites may mechanically become `max([A, B])`. Metadata names and +elapsed units then require the explicit rewrites above. diff --git a/context/effect-4/recipes/schema-annotable-top.md b/context/effect-4/recipes/schema-annotable-top.md new file mode 100644 index 0000000000..fc16e0142f --- /dev/null +++ b/context/effect-4/recipes/schema-annotable-top.md @@ -0,0 +1,30 @@ +# Pattern: schema-annotable-top + +**Area:** Schema type bounds **Kind:** mechanical + +## Mapping + +Effect 4 removes the `Schema.Annotable` namespace. For generic helpers that accepted any annotatable +schema, replace the v3 bound with `Schema.Top`: + +```ts +// v3 +const annotate = (schema: S): S => schema + +// v4 +const annotate = (schema: S): S => schema +``` + +`Schema.Top` is the beta.102 top-level schema interface and retains the schema's precise rebuilt type +through annotation methods. + +## Verification + +Checked against `effect@4.0.0-beta.102`: `Schema.Annotable` is absent and `Schema.Top` is declared in +`Schema.ts`. + +## Gotchas + +- Use this mapping for a type bound, not as a replacement runtime value. +- Preserve the original generic `S`; widening parameters or returns to `Schema.Top` loses the + caller's concrete schema type. diff --git a/context/effect-4/recipes/schema-ast-introspection.md b/context/effect-4/recipes/schema-ast-introspection.md new file mode 100644 index 0000000000..b53012a7a0 --- /dev/null +++ b/context/effect-4/recipes/schema-ast-introspection.md @@ -0,0 +1,109 @@ +# Pattern: SchemaAST introspection + +**Area:** Schema AST tooling **Kind:** representation and shape change **Our usage:** schema-driven +form metadata, tagged-struct detection, property enumeration, and other direct AST consumers. + +## Shape changes first + +Effect 4 does not rename the Effect 3 wrapper nodes one-for-one. The decoded node is the AST root: + +- refinements are `ast.checks`; +- transformations are `ast.encoding`; +- each encoding `Link.to` is the encoded side, not the decoded replacement; +- `SchemaAST.toType(ast)` strips encodings and returns the decoded view. + +Do not migrate `Transformation.to` by traversing `Link.to`; that reverses the question and inspects +the wire representation. + +## Node and property mappings + +| Effect 3 | Effect 4 | +| --- | --- | +| `StringKeyword` | `String` | +| `NumberKeyword` | `Number` | +| `BooleanKeyword` | `Boolean` | +| `UndefinedKeyword` | `Undefined` | +| `TupleType` | `Arrays` | +| `TypeLiteral` | `Objects` | +| `Enums` | `Enum` | +| `Refinement.from` | no node traversal; checks are attached to the decoded node | +| `Transformation.to` | `SchemaAST.toType(ast)` | +| `PropertySignature.isOptional` | `SchemaAST.isOptional(property.type)` | +| `PropertySignature.annotations` | `property.type.context?.annotations` | + +`Objects.propertySignatures` retains declaration order and each property remains a `{ name, type }` +pair. + +## Optional values versus optional keys + +V4 key optionality lives in `ast.context?.isOptional` and is exposed by +`SchemaAST.isOptional(ast)`. That alone is not a faithful replacement for an introspector that also +classified `Schema.UndefinedOr(S)` as optional. + +For that behavior, additionally recognize an exact two-member `Union` containing one `Undefined` +member and return the other member as the inner schema. Do not classify wider unions this way. + +## Annotations + +Value-level annotation IDs became string-keyed resolvers: + +```ts +SchemaAST.resolveTitle(ast) +SchemaAST.resolveDescription(ast) +``` + +Repository-owned symbol annotations remain present in the resolved annotation object, but the +public type exposes only string keys. Read a unique-symbol key through a narrow helper: + +```ts +const resolveAnnotation = (ast: SchemaAST.AST, key: symbol): T | undefined => + (SchemaAST.resolve(ast) as unknown as Record | undefined)?.[key] as + | T + | undefined +``` + +Key/property annotations live at `property.type.context?.annotations` and take precedence over +value annotations when both exist. + +Effect 4 no longer supplies all implicit primitive metadata that Effect 3 exposed. For example, +`Schema.Int` retains `expected: "an integer"` on its final check but does not expose the former +implicit `title: "int"` and `description: "an integer"` pair. Do not invent fallback titles or +silently update UI baselines; measure the owning package's label impact and record an explicit +decision. + +## Any schema without services + +`Schema.Top` is too wide as a replacement for `Schema.Schema.AnyNoContext`: its decoding and +encoding service slots are `unknown`, so it admits serviceful codecs. + +Use the service-free bound: + +```ts +type AnyNoContext = Schema.Codec +``` + +## Equivalence + +The mappings above were checked against the `effect@4.0.0-beta.102` declarations and runtime ASTs +for annotated primitives, checks, encodings, `UndefinedOr`, `optional`, `optionalKey`, literal +unions, structs, tuples, and `NumberFromString`. + +Owning slices must compare the questions their introspector answers: decoded field classification, +property order, optional-key and undefined-value handling, literal order, tagged detection, +resolved metadata, and any exposed raw AST representation. + +## Gotchas + +- `Link.to` is the encoded side. +- Checks do not introduce a wrapper node, so a checked number still has `_tag: "Number"`. +- `SchemaAST.isOptional` detects key optionality, not every union that accepts `undefined`. +- `SchemaAST.resolveTitle` and `resolveDescription` read value/check annotations, not key context. +- Indexing the public result of `SchemaAST.resolve` with a unique symbol requires the narrow + `Record` cast above; do not widen the rest of the AST consumer. +- Raw `_tag` values and implicit primitive metadata differ even when field classification is + preserved. + +## Codemod rule + +Only the direct tag and resolver renames are mechanical. Transformation direction, optionality, +key-annotation precedence, service bounds, and user-visible metadata require runtime verification. diff --git a/context/effect-4/recipes/schema-codec-type-contract.md b/context/effect-4/recipes/schema-codec-type-contract.md new file mode 100644 index 0000000000..de69895307 --- /dev/null +++ b/context/effect-4/recipes/schema-codec-type-contract.md @@ -0,0 +1,86 @@ +# Pattern: schema-codec-type-contract + +**Area:** Schema type-level contracts **Kind:** shape change **Our usage:** generic codec helpers +throughout the repository, including 23 multi-parameter references in `notion-effect-schema` and +one-parameter public return types in `otel-contract`. + +## Shape change + +Effect 3 used one `Schema.Schema` type for both value-only schemas and bidirectional codecs: + +```ts +Schema.Schema +``` + +Effect 4's `Schema.Schema` is a different, weaker interface that carries only the decoded +type. The faithful replacement is `Schema.Codec` at **every arity**: + +| v3 | v4 | +| ------------------------ | -------------------------- | +| `Schema.Schema` | `Schema.Codec` | +| `Schema.Schema` | `Schema.Codec` | +| `Schema.Schema` | `Schema.Codec` | + +`Codec` defaults `E = T`, `RD = never`, and `RE = never`, exactly matching the omitted Effect 3 +parameters. For the three-parameter form, Effect 3's single `Context` applied in both directions, +so it must be copied into v4's decoding and encoding service slots. + +All interface shapes and defaults above are **VERIFIED** against the real +`effect@4.0.0-beta.102` tarball (SHA-1 +`f51092854960f60cbdb06bd59e788acbc8ee8492`). + +## Why the alternatives are not equivalent + +- `Schema.Schema` drops the encoded type and both service requirements, even when the v3 type + used only one explicit parameter. +- `Schema.Codec` narrows the encoding requirement. +- `Schema.Codec` narrows the decoding requirement. +- `Schema.ConstraintCodec` is only for APIs that read type views and do not require the full schema + protocol. +- `Schema.Decoder` and `Schema.Encoder` are deliberately one-directional. + +Even when a current helper exercises only one direction, narrowing the other direction is a +refactor rather than a faithful port and belongs in follow-up work. + +## `AnyNoContext` + +The replacement for v3 `Schema.Schema.AnyNoContext` is: + +```ts +Schema.Codec +``` + +Do **not** use `Schema.Top`. `Top` erases both service slots to `unknown` and therefore admits +codecs that require decoding or encoding services; v3 `AnyNoContext` rejected them. + +This bound is **VERIFIED** from both tarball declarations: + +```text +v3 AnyNoContext = Schema +v4 Codec +v4 Top = decoded/encoded/services all unknown +``` + +Use `Schema.ConstraintCodec` only when the generic utility merely reads +type views and does not call schema methods. A direct port of a v3 `Schema` bound should retain the +full protocol with `Codec`. + +## Equivalence + +This recipe preserves the v3 compile-time encoded and service contracts. It does not apply a +repository migration or establish runtime codec equivalence. Owning slices must replay decoding +and encoding with exact encoded bytes and both service environments. + +## Gotchas + +- The apparently safe one-parameter spelling is the easiest trap: v4 `Schema` is not v3 + `Schema` with fewer visible parameters. +- The two service slots are easy to confuse because both default to `never`. +- A green typecheck after narrowing one slot does not prove the old public contract was preserved. +- `Top` is the existential any-schema bound, not the service-free-codec bound. + +## Codemod rule + +All v3 `Schema.Schema<...>` type references become `Schema.Codec<...>`. Duplicate the third v3 +parameter into both v4 service slots. Replace `Schema.Schema.AnyNoContext` with +`Schema.Codec`. diff --git a/context/effect-4/recipes/schema-date.md b/context/effect-4/recipes/schema-date.md index bd4f4a4929..b9086b66bf 100644 --- a/context/effect-4/recipes/schema-date.md +++ b/context/effect-4/recipes/schema-date.md @@ -1,7 +1,22 @@ # Pattern: schema-date -**Area:** Schema wire format **Kind:** semantic (conditional rewrite) **Our usage:** `Schema.DateTimeUtc`, -`Schema.DateFromSelf`, and date transforms appear in agent-session, Notion codecs, and path schemas. +**Area:** Schema wire format **Kind:** semantic (conditional rewrite) **Our usage:** +`Schema.DateTimeUtc`, `Schema.DateFromSelf`, and date transforms appear in agent-session, Notion +codecs, and path schemas. + +## Shape change first: surviving names changed meaning + +Effect 4 split v3 string-to-value schemas into a bare instance validator plus a +`...FromString` wire codec: + +| v3 wire schema | v4 wire codec | Surviving v4 bare name | +| -------------------- | ------------------------------ | -------------------------------------------------------- | +| `Schema.Date` | `Schema.DateFromString` | `Schema.Date` validates a `Date` instance | +| `Schema.DateTimeUtc` | `Schema.DateTimeUtcFromString` | `Schema.DateTimeUtc` validates a `DateTime.Utc` instance | + +This is the dangerous pattern: the old identifier survives, typechecks in some generic positions, +and silently stops accepting or emitting the old string representation. Renames fail loudly; +survivals require wire-boundary review. ## v3 @@ -20,6 +35,16 @@ const encode = Schema.encodeExit(DateWire) The v4 codecs return `Exit`, not `Either`; callers must handle the changed result shape. +For UTC `DateTime` wire sites: + +```ts +// v3 +Schema.DateTimeUtc + +// v4 +Schema.DateTimeUtcFromString +``` + ## Equivalence Command: @@ -33,6 +58,10 @@ using `Schema.DateFromString` on effect `4.0.0-beta.102`. The only differences are allowlisted parse-message text changes for invalid inputs. That allowlist is a harness classification, not blanket migration acceptance. +`Schema.DateTimeUtcFromString` is also **VERIFIED** against beta.102. Its encode path uses +`DateTime.formatIso`, which reaches `toDateUtc(self).toISOString()`. For the characterized value it +emits the exact v3 bytes `"2026-05-25T10:00:00.000Z"`. + ## Intended differences (alignment register entries) - v4 invalid-date parse messages use `SchemaError(...)`; v3 prints a parse-tree @@ -50,6 +79,8 @@ a harness classification, not blanket migration acceptance. - This is a rename trap: `Schema.Date` still exists in v4 but means a Date instance schema, not the v3 ISO-string wire schema. +- `Schema.DateTimeUtc` is the same trap for `DateTime.Utc`: the bare v4 name validates an instance; + use `DateTimeUtcFromString` for v3 string-to-value wire behavior. - In effect `4.0.0-beta.102`, `Schema.Date` rejects invalid Date instances and `Schema.DateFromString` decodes through that stricter Date schema. The beta.99 mapping `Schema.DateFromString.check(Schema.isDateValid())` is stale because diff --git a/context/effect-4/recipes/schema-json-formatting.md b/context/effect-4/recipes/schema-json-formatting.md new file mode 100644 index 0000000000..0c721a893f --- /dev/null +++ b/context/effect-4/recipes/schema-json-formatting.md @@ -0,0 +1,59 @@ +# Pattern: schema-json-formatting + +**Area:** Schema JSON wire bytes **Kind:** semantic boundary **Our usage:** persisted configuration +and generated JSON files that pass `{ space }` to v3 JSON codecs. + +## Shape change + +V4 has no pretty-print JSON codec. `Schema.fromJsonString(S)` accepts no formatting options, and no +beta.102 codec replacement carries v3's `{ space }` behavior. + +The replacement is to encode with the schema, then stringify the encoded JSON value explicitly: + +```ts +// v3 +const JsonConfig = Schema.parseJson(Config, { space: 2 }) +const content = Schema.encodeSync(JsonConfig)(value) + +// v4 +const encoded = Schema.encodeSync(Config)(value) +const content = JSON.stringify(encoded, null, 2) +``` + +The same rule applies to v3 `Schema.fromJsonString(S, { space })` call shapes. Use +`Schema.encodeEffect` when the surrounding path handles schema failures as an Effect. + +This absence and replacement are **VERIFIED** against the real +`effect@4.0.0-beta.102` tarball. + +## Equivalence + +Formatting is behavior at persistence, CLI, snapshot, and hashing boundaries. Compare the complete +output bytes, including indentation, property order, escaping, and final newline. + +`JSON.stringify(encoded, null, 2)` preserves a v3 two-space formatting request only after +`encoded` has passed through the owning schema. Stringifying the decoded value directly can bypass +schema transformations and emit different bytes. + +Preserve any existing trailing newline separately: + +```ts +const content = `${JSON.stringify(encoded, null, 2)}\n` +``` + +## Intended differences + +None. File churn is not an accepted migration difference. + +## Gotchas + +- `Schema.fromJsonString(S)` remains the v4 parse/stringify codec for compact JSON, but it has no + pretty-print option. +- Do not stringify the decoded application value when the schema transforms its encoded shape. +- `space` may be a number or string in `JSON.stringify`; preserve the exact v3 setting. +- A value-level round trip cannot prove whitespace bytes. + +## Codemod rule + +No broad codemod. Split the old combined codec operation at each formatted output boundary: +schema-encode first, then explicit `JSON.stringify`, then preserve any newline policy. diff --git a/context/effect-4/recipes/schema-struct-extension.md b/context/effect-4/recipes/schema-struct-extension.md new file mode 100644 index 0000000000..13392e5615 --- /dev/null +++ b/context/effect-4/recipes/schema-struct-extension.md @@ -0,0 +1,67 @@ +# Pattern: schema-struct-extension + +**Area:** Schema struct composition **Kind:** shape change **Our usage:** 27 sites in +`notion-effect-schema`. + +## Shape change first + +Effect 4 has no standalone `Schema.extend`. There are two replacements, selected by the right-hand +schema's shape. + +### Plain field merge + +For two plain structs: + +```ts +// v3 +Schema.extend(A, B) + +// v4 +Schema.Struct({ ...A.fields, ...B.fields }) +``` + +This mirrors the field merge used internally by Effect 4's Class `static extend` implementation. +It applies to plain `Struct` and `TaggedStruct` values; it does not require or create a Class. + +### Struct plus index signature + +For a struct extended with a record: + +```ts +// v3 +Schema.extend(BlockBase, Schema.Record({ key: Schema.String, value: Schema.Unknown })) + +// v4 +Schema.StructWithRest(BlockBase, [Schema.Record(Schema.String, Schema.Unknown)]) +``` + +`Schema.Record` returns `$Record`, not `Struct`, and has no `.fields` to spread. Treating it like a +plain struct silently drops the index signature and changes excess-property decoding. + +## Not a replacement: `extendTo` + +Do not replace a plain merge with `Schema.extendTo`. `extendTo(fields, derive)` computes derived +fields from decoded input using `Option`-returning callbacks. It is a transforming schema +operation, not a struct merge. + +## Equivalence + +Both replacements and Effect 4's own Class merge implementation are **VERIFIED** against the real +`effect@4.0.0-beta.102` tarball. + +The measured 27 `notion-effect-schema` sites comprise 25 plain struct/tagged-struct merges and two +struct-plus-record extensions. Owning slices must replay encoded baselines. For `StructWithRest`, +include an additional property and verify that decode and encode preserve the same key and bytes. + +## Gotchas + +- `Schema.Record(...).fields` is not a valid merge; `Record` is not a `Struct`. +- `Schema.extendTo` can typecheck while implementing different runtime behavior. +- Field order follows object spread order. Preserve the original left-to-right extension order. +- When both structs define the same key, the right-hand fields continue to win. + +## Codemod rule + +Only plain `Struct`/`TaggedStruct` pairs may use the field-spread rewrite. Classify every right-hand +operand first. Record/index-signature operands require `StructWithRest`; Class and genuinely +derived-field sites require separate adjudication. diff --git a/context/effect-4/recipes/schema-transformations-issues.md b/context/effect-4/recipes/schema-transformations-issues.md index 71c82a2c36..2088af729b 100644 --- a/context/effect-4/recipes/schema-transformations-issues.md +++ b/context/effect-4/recipes/schema-transformations-issues.md @@ -48,6 +48,32 @@ const NumberFromString = Schema.String.pipe( The source schema is piped into `decodeTo`; the target schema is its first argument. The old `strict` option is not copied. +### Decode-only pure transformations + +When an unsupported encode callback only throws, its inferred return type is `never`. Do not leave +the transformation's encoded type to inference: state both transformation types and annotate the +throwing callback's return type. + +```ts +const DecodeOnly = Schema.String.pipe( + Schema.decodeTo( + Target, + SchemaTransformation.transform({ + decode: (input) => decodeInput(input), + encode: (_value): string => { + throw new Error('This codec only supports decoding') + }, + }), + ), +) +``` + +This explicit form is **VERIFIED** to compile against the +`effect@4.0.0-beta.102` declarations and retains the encoded `string` contract. Prefer +`transformOrFail` with a `SchemaIssue.Forbidden` failure when the old codec represented unsupported +encoding in the typed error channel. Whether throwing or typed failure is faithful depends on the +v3 call site; do not change that boundary merely to satisfy inference. + ## Fallible transformations and issues ### v3 @@ -118,6 +144,34 @@ Use `Schema.refine(typeGuard)` when the callback narrows the TypeScript type. `m a string, a `SchemaIssue.Issue`, `{ path, issue }`, or an array of issues when error structure must be preserved. +### Filter messages are no longer lazy + +V3 filter/refinement annotations accepted `message: () => string`. Beta.102 +`Annotations.Filter.message` is a plain `string`; there is no lazy annotation form. This is +**VERIFIED** from the beta.102 declaration. + +For the repository's closures that return a constant string, preserve the text as a constant: + +```ts +// v3 +Schema.filter(predicate, { message: () => 'must be valid' }) + +// v4 +Schema.refine(predicate, { message: 'must be valid' }) +``` + +Do not mechanically invoke a stateful or expensive closure while constructing the schema. That +changes evaluation from failure-time to schema-construction-time. If narrowing is not required, a +lazy failure string can instead be produced by the check itself: + +```ts +Schema.check(Schema.makeFilter((value) => predicate(value) || message())) +``` + +That form does not preserve a type-guard narrowing. A stateful/expensive type-guard message +therefore needs explicit design review; beta.102 has no direct replacement that preserves both +narrowing and lazy message evaluation. + ## Affected inventory The AST audit measured 27 `ParseResult` references in 15 files and 7 packages. The namespace-member @@ -153,12 +207,16 @@ SHA-1 `f51092854960f60cbdb06bd59e788acbc8ee8492`. - `SchemaTransformation.transform` is only for pure, infallible conversions. Returning an `Effect` from it creates the wrong decoded value. +- Give decode-only pure transformations explicit `` parameters and an encoded return + annotation on a throwing callback; otherwise `never` can erase the intended direction. - `SchemaTransformation.transformOrFail` callbacks return `Effect`; throwing bypasses the typed issue channel. - `Schema.decodeUnknownEffect` exposes `SchemaError`, while transformation callbacks operate on `SchemaIssue.Issue`. - A formatter rewrite can change user-visible text even when decoding behavior is otherwise equivalent. +- V4 filter annotation messages are eager strings. Only constant closures can be collapsed without + a timing/value review. ## Codemod rule diff --git a/context/effect-4/recipes/schema-utility-renames.md b/context/effect-4/recipes/schema-utility-renames.md new file mode 100644 index 0000000000..58b8d0661a --- /dev/null +++ b/context/effect-4/recipes/schema-utility-renames.md @@ -0,0 +1,32 @@ +# Pattern: schema-utility-renames + +**Area:** Schema utilities **Kind:** mechanical + +## Mapping + +```ts +// v3 +Schema.asSchema(MySchema) +Schema.equivalence(MySchema) + +// v4 +MySchema +Schema.toEquivalence(MySchema) +``` + +`Schema.asSchema` is removed; where it only wrapped an already-typed schema, remove the wrapper. +`Schema.equivalence` is renamed to `Schema.toEquivalence`. + +If a site supplies a custom equivalence implementation, use `Schema.overrideToEquivalence`; that API +annotates a schema and is not a replacement for deriving an equivalence. + +## Verification + +Checked against `effect@4.0.0-beta.102`: `Schema.asSchema` and `Schema.equivalence` are absent; +`Schema.toEquivalence` and `Schema.overrideToEquivalence` are declared in `Schema.ts`. + +## Gotchas + +- Removing `asSchema` is mechanical only when its input is already a schema. Stop if the wrapper was + participating in inference or accepting a non-schema value. +- Do not replace a custom-equivalence site with `toEquivalence`; preserve the override. diff --git a/context/effect-4/recipes/schema-uuid-guid.md b/context/effect-4/recipes/schema-uuid-guid.md new file mode 100644 index 0000000000..8ea77ad7d3 --- /dev/null +++ b/context/effect-4/recipes/schema-uuid-guid.md @@ -0,0 +1,56 @@ +# Pattern: schema-uuid-guid + +**Area:** Schema string checks **Kind:** removal with a surviving-name trap **Our usage:** two +packages (`otel-contract` and `utils`) + +## Shape change first: UUID was a GUID check + +`Schema.UUID` is removed in Effect 4 beta.102. The faithful replacement is: + +```ts +Schema.String.check(Schema.isGUID()) +``` + +Do **not** replace it with the similarly named `Schema.isUUID()`. Effect 3's `Schema.UUID` accepted +any dashed hexadecimal `8-4-4-4-12` GUID. Effect 4's `isUUID` additionally enforces UUID version and +variant nibbles, so it silently tightens the accepted set. + +For example, Effect 3 accepts `123e4567-e89b-02d3-0456-426614174000`, while Effect 4's `isUUID` +rejects it. Effect 4's `isGUID` accepts it and preserves the encoded string. + +## v3 + +```ts +const RequestId = Schema.UUID +``` + +## v4 + +```ts +const RequestId = Schema.String.check(Schema.isGUID()) +``` + +## Equivalence + +The replacement was checked at runtime against `effect@3.21.4` and +`effect@4.0.0-beta.102`. Re-run the comparison with: + +- a standard versioned lowercase UUID; +- the same value in uppercase; +- the nil UUID; +- an all-`f` GUID; +- `123e4567-e89b-02d3-0456-426614174000`, whose version and variant nibbles are invalid; +- a 32-character hexadecimal value without dashes; +- a non-GUID string. + +For each accepted value, also compare the encoded strings exactly. In the measured cases, v3 +`Schema.UUID` and v4 `Schema.String.check(Schema.isGUID())` had identical acceptance and encoding; +v4 `Schema.isUUID()` rejected the non-version/variant counterexample. + +## Gotchas + +- The symbol whose name most resembles the v3 schema is the wrong replacement. +- A typecheck cannot detect the accepted-set tightening. +- Do not add a brand merely because the old export was named `UUID`; v3's filter decoded to a plain + string. + diff --git a/context/effect-4/recipes/scope-closeable.md b/context/effect-4/recipes/scope-closeable.md new file mode 100644 index 0000000000..cf1e388a89 --- /dev/null +++ b/context/effect-4/recipes/scope-closeable.md @@ -0,0 +1,28 @@ +# Pattern: scope-closeable + +**Area:** Scope **Kind:** mechanical + +## Mapping + +`Scope.CloseableScope` is renamed to `Scope.Closeable` in Effect 4: + +```ts +// v3 +let scope: Scope.CloseableScope + +// v4 +let scope: Scope.Closeable +``` + +The beta.102 declaration keeps `Closeable` as an interface extending `Scope`; scope creation and +closing behavior are unchanged by this type rename. + +## Verification + +Checked against `effect@4.0.0-beta.102` `Scope.ts`, where `Closeable` is declared and +`CloseableScope` is absent. + +## Gotchas + +- This is only the closeable-scope type rename. It does not change `Scope.make`, finalizer order, or + the `Scope.close` exit supplied by the caller. diff --git a/context/effect-4/recipes/sink-reduce.md b/context/effect-4/recipes/sink-reduce.md new file mode 100644 index 0000000000..8728274d5b --- /dev/null +++ b/context/effect-4/recipes/sink-reduce.md @@ -0,0 +1,52 @@ +# Pattern: sink-reduce + +**Area:** Stream sinks **Kind:** constructor rename with evaluation-shape change **Our usage:** +constant-memory git status line counters in `megarepo`. + +## v3 + +```ts +Sink.foldLeft(0, (count, line) => (line.trim() === '' ? count : count + 1)) +``` + +## v4 + +```ts +Sink.reduce( + () => 0, + (count, line) => (line.trim() === '' ? count : count + 1), +) +``` + +V4 `Sink.reduce` is the direct pure, consume-all-input replacement. Its initial state is a +`LazyArg`, so the seed is supplied as a function. + +## Equivalence + +The constructor signatures are **VERIFIED** against the real tarballs. A cross-major stream probe +over empty, non-empty, and whitespace-only strings produced the same final count (`2`). + +Both forms consume the entire stream, update once per element, retain no leftovers, and use +constant accumulator memory. + +## Seed-evaluation trap + +V3 receives an already-evaluated seed. V4 evaluates its seed once per sink run. For a dynamic or +effectful-looking expression, preserve v3 timing by hoisting: + +```ts +const seed = makeSeed() +const sink = Sink.reduce(() => seed, update) +``` + +Writing `Sink.reduce(() => makeSeed(), update)` intentionally changes construction-time evaluation +into per-run evaluation and may create fresh mutable state. + +## Intended differences + +None. + +## Codemod rule + +`Sink.foldLeft(seed, update)` becomes `Sink.reduce(() => seed, update)`. Hoist non-trivial seed +expressions first when their original evaluation timing matters. diff --git a/context/effect-4/recipes/stream-text-decoding.md b/context/effect-4/recipes/stream-text-decoding.md new file mode 100644 index 0000000000..a3ce159230 --- /dev/null +++ b/context/effect-4/recipes/stream-text-decoding.md @@ -0,0 +1,54 @@ +# Pattern: stream-text-decoding + +**Area:** Stream byte decoding **Kind:** call-shape change **Our usage:** git/process stdout and +stderr decoding in `megarepo`, `utils`, `utils-dev`, and `effect-ai-claude-cli`. + +## v3 + +```ts +bytes.pipe(Stream.decodeText('utf-8')) +``` + +## v4 + +```ts +bytes.pipe(Stream.decodeText({ encoding: 'utf-8' })) +``` + +The encoding moved from a positional string to an options object. The no-options UTF-8 forms remain: + +```ts +bytes.pipe(Stream.decodeText) +bytes.pipe(Stream.decodeText()) +``` + +## Equivalence + +The beta.102 signature and TextDecoder implementation are **VERIFIED** against the real tarball. A +cross-major probe split the four-byte UTF-8 encoding of `😀` across two `Uint8Array` chunks. Both +majors emitted exactly: + +```json +["A", "😀B"] +``` + +This proves the replacement retains streaming decoder state across chunk boundaries for the tested +UTF-8 input. Owning process/CLI slices must compare complete stdout and stderr bytes separately, +including malformed-sequence handling where relevant. + +## Intended differences + +None. + +## Gotchas + +- Do not drop a non-default encoding while fixing the call shape. +- Constructing a new `TextDecoder` independently for every chunk corrupts multi-byte sequences + split across chunks. Keep the Stream decoder. +- Text chunk boundaries are not a stable external contract; compare concatenated bytes/text unless + the application explicitly consumes chunking. + +## Codemod rule + +`Stream.decodeText(encoding)` becomes `Stream.decodeText({ encoding })`. Calls that pass the stream +as the first argument become `Stream.decodeText(stream, { encoding })`. diff --git a/context/effect-4/recipes/terminal-prompt-pty.md b/context/effect-4/recipes/terminal-prompt-pty.md index a922c8a2dd..ffdcf2e06e 100644 --- a/context/effect-4/recipes/terminal-prompt-pty.md +++ b/context/effect-4/recipes/terminal-prompt-pty.md @@ -15,7 +15,7 @@ import { NodeContext } from '@effect/platform-node' ## v4 ```ts -import { Terminal } from 'effect' +import { Terminal } from 'effect/Terminal' import { Prompt } from 'effect/unstable/cli' import { NodeServices } from '@effect/platform-node' ``` diff --git a/context/effect-4/recipes/vitest-collection.md b/context/effect-4/recipes/vitest-collection.md index 7ed46f28ce..f329f1e37e 100644 --- a/context/effect-4/recipes/vitest-collection.md +++ b/context/effect-4/recipes/vitest-collection.md @@ -58,3 +58,34 @@ effect/FastCheck -> effect/testing/FastCheck ``` Keep runner imports from `@effect/vitest`; do not rewrite them to `effect/testing`. + +## Scoped test methods: one axis disappeared + +V3 test methods varied on two independent axes: + +```text +environment: TestEnv / test clock <-> live services / real clock +scope: unscoped <-> scoped +``` + +Beta.102 always provides a runner-owned `Scope`, so only the environment/clock axis remains: + +| v3 | v4 | Clock | Scope | +| --------------- | ----------- | ---------- | -------- | +| `it.scoped` | `it.effect` | test clock | provided | +| `it.scopedLive` | `it.live` | real clock | provided | + +This is **VERIFIED** against the installed beta.102 declarations and implementation: + +- `MethodsNonLive.effect` is `Tester` and runs `Effect.scoped` before providing + `TestEnv`; +- `Methods.live` is `Tester` and is built with `Effect.scoped`; +- concrete `otelite` tests call effects requiring `Scope` under `it.live`. + +The migration is not “remove `scoped` wherever it appears.” Preserve the clock half: +`scoped -> live` silently swaps the test clock for real time, while `scopedLive -> effect` silently +does the reverse. Either can compile and then change timeout, sleep, retry, or scheduling behavior. + +For scoped test migrations, replay resource finalizer count/order and the timing behavior exercised +by the test. A test that merely starts successfully proves neither clock selection nor scope +teardown. diff --git a/context/effect/socket/examples/http-ws-combined.ts b/context/effect/socket/examples/http-ws-combined.ts index e1e2b98740..3aca4f982e 100644 --- a/context/effect/socket/examples/http-ws-combined.ts +++ b/context/effect/socket/examples/http-ws-combined.ts @@ -3,13 +3,13 @@ import { createServer } from 'node:http' import { NodeRuntime } from '@effect/platform-node' import { layer as nodeHttpLayer } from '@effect/platform-node/NodeHttpServer' import { layerWebSocket } from '@effect/platform-node/NodeSocketServer' -import { catchAllCause, empty, get } from '@effect/platform/HttpRouter' -import { serveEffect } from '@effect/platform/HttpServer' -import { text } from '@effect/platform/HttpServerResponse' -import type { CloseEvent, Socket as SocketType } from '@effect/platform/Socket' -import { toChannelString } from '@effect/platform/Socket' -import type { Address } from '@effect/platform/SocketServer' -import { SocketServer } from '@effect/platform/SocketServer' +import { catchAllCause, empty, get } from 'effect/unstable/http/HttpRouter' +import { serveEffect } from 'effect/unstable/http/HttpServer' +import { text } from 'effect/unstable/http/HttpServerResponse' +import type { CloseEvent, Socket as SocketType } from 'effect/unstable/socket/Socket' +import { toChannelString } from 'effect/unstable/socket/Socket' +import type { Address } from 'effect/unstable/socket/SocketServer' +import { SocketServer } from 'effect/unstable/socket/SocketServer' import { Effect, Layer, Stream } from 'effect' /** diff --git a/context/effect/socket/examples/rpc-schema.ts b/context/effect/socket/examples/rpc-schema.ts index 81e69d71ff..f25a270f60 100644 --- a/context/effect/socket/examples/rpc-schema.ts +++ b/context/effect/socket/examples/rpc-schema.ts @@ -1,5 +1,5 @@ -import { make as makeRpc } from '@effect/rpc/Rpc' -import { make as makeRpcGroup } from '@effect/rpc/RpcGroup' +import { make as makeRpc } from 'effect/unstable/rpc/Rpc' +import { make as makeRpcGroup } from 'effect/unstable/rpc/RpcGroup' import { Schema } from 'effect' /** diff --git a/context/effect/socket/examples/rpc-ws-client.ts b/context/effect/socket/examples/rpc-ws-client.ts index 220589ab29..848c079808 100644 --- a/context/effect/socket/examples/rpc-ws-client.ts +++ b/context/effect/socket/examples/rpc-ws-client.ts @@ -1,7 +1,7 @@ import { NodeRuntime } from '@effect/platform-node' -import { layerWebSocket, layerWebSocketConstructorGlobal } from '@effect/platform/Socket' -import { layerProtocolSocket, make as makeRpcClient } from '@effect/rpc/RpcClient' -import { layerJson } from '@effect/rpc/RpcSerialization' +import { layerWebSocket, layerWebSocketConstructorGlobal } from 'effect/unstable/socket/Socket' +import { layerProtocolSocket, make as makeRpcClient } from 'effect/unstable/rpc/RpcClient' +import { layerJson } from 'effect/unstable/rpc/RpcSerialization' import { Effect, Layer } from 'effect' import { Api } from './rpc-schema.ts' diff --git a/context/effect/socket/examples/rpc-ws-server.ts b/context/effect/socket/examples/rpc-ws-server.ts index b7c4bba5d9..17e9d45d10 100644 --- a/context/effect/socket/examples/rpc-ws-server.ts +++ b/context/effect/socket/examples/rpc-ws-server.ts @@ -1,7 +1,7 @@ import { NodeRuntime } from '@effect/platform-node' import { layerWebSocket } from '@effect/platform-node/NodeSocketServer' -import { layerJson } from '@effect/rpc/RpcSerialization' -import { layer, layerProtocolSocketServer } from '@effect/rpc/RpcServer' +import { layerJson } from 'effect/unstable/rpc/RpcSerialization' +import { layer, layerProtocolSocketServer } from 'effect/unstable/rpc/RpcServer' import { Effect, Layer } from 'effect' import { Api } from './rpc-schema.ts' diff --git a/context/effect/socket/examples/tcp-echo-client.ts b/context/effect/socket/examples/tcp-echo-client.ts index 8e6b970251..b2a71bb7b3 100644 --- a/context/effect/socket/examples/tcp-echo-client.ts +++ b/context/effect/socket/examples/tcp-echo-client.ts @@ -1,6 +1,6 @@ import { NodeRuntime } from '@effect/platform-node' import { makeNet } from '@effect/platform-node/NodeSocket' -import { CloseEvent } from '@effect/platform/Socket' +import { CloseEvent } from 'effect/unstable/socket/Socket' import { Duration, Effect, Fiber } from 'effect' /** diff --git a/context/effect/socket/examples/tcp-echo-server.ts b/context/effect/socket/examples/tcp-echo-server.ts index 8b6812ed46..147229efeb 100644 --- a/context/effect/socket/examples/tcp-echo-server.ts +++ b/context/effect/socket/examples/tcp-echo-server.ts @@ -1,8 +1,8 @@ import { NodeRuntime } from '@effect/platform-node' import { layer } from '@effect/platform-node/NodeSocketServer' -import type { Socket as SocketType } from '@effect/platform/Socket' -import type { Address } from '@effect/platform/SocketServer' -import { SocketServer } from '@effect/platform/SocketServer' +import type { Socket as SocketType } from 'effect/unstable/socket/Socket' +import type { Address } from 'effect/unstable/socket/SocketServer' +import { SocketServer } from 'effect/unstable/socket/SocketServer' import { Effect } from 'effect' /** diff --git a/context/effect/socket/examples/ws-broadcast-client.ts b/context/effect/socket/examples/ws-broadcast-client.ts index e51edddcfc..0d5a6514a3 100644 --- a/context/effect/socket/examples/ws-broadcast-client.ts +++ b/context/effect/socket/examples/ws-broadcast-client.ts @@ -1,11 +1,11 @@ import { NodeRuntime } from '@effect/platform-node' -import type { Socket as SocketType } from '@effect/platform/Socket' +import type { Socket as SocketType } from 'effect/unstable/socket/Socket' import { CloseEvent, layerWebSocketConstructorGlobal, makeWebSocket, toChannelString, -} from '@effect/platform/Socket' +} from 'effect/unstable/socket/Socket' import { Duration, Effect, Fiber, Stream } from 'effect' /** diff --git a/context/effect/socket/examples/ws-broadcast-server.ts b/context/effect/socket/examples/ws-broadcast-server.ts index 65df47c794..48367443cd 100644 --- a/context/effect/socket/examples/ws-broadcast-server.ts +++ b/context/effect/socket/examples/ws-broadcast-server.ts @@ -1,9 +1,9 @@ import { NodeRuntime } from '@effect/platform-node' import { layerWebSocket } from '@effect/platform-node/NodeSocketServer' -import type { CloseEvent, Socket as SocketType } from '@effect/platform/Socket' -import { toChannelString } from '@effect/platform/Socket' -import type { Address } from '@effect/platform/SocketServer' -import { SocketServer } from '@effect/platform/SocketServer' +import type { CloseEvent, Socket as SocketType } from 'effect/unstable/socket/Socket' +import { toChannelString } from 'effect/unstable/socket/Socket' +import type { Address } from 'effect/unstable/socket/SocketServer' +import { SocketServer } from 'effect/unstable/socket/SocketServer' import { Effect, Fiber, PubSub, Stream } from 'effect' /** diff --git a/context/effect/socket/examples/ws-echo-client.ts b/context/effect/socket/examples/ws-echo-client.ts index bd97f3f2b5..5078b1b928 100644 --- a/context/effect/socket/examples/ws-echo-client.ts +++ b/context/effect/socket/examples/ws-echo-client.ts @@ -1,11 +1,11 @@ import { NodeRuntime } from '@effect/platform-node' -import type { Socket as SocketType } from '@effect/platform/Socket' +import type { Socket as SocketType } from 'effect/unstable/socket/Socket' import { CloseEvent, layerWebSocketConstructorGlobal, makeWebSocket, toChannelString, -} from '@effect/platform/Socket' +} from 'effect/unstable/socket/Socket' import { Duration, Effect, Fiber, Stream } from 'effect' /** diff --git a/context/effect/socket/examples/ws-echo-server.ts b/context/effect/socket/examples/ws-echo-server.ts index 397a96b579..9980a818aa 100644 --- a/context/effect/socket/examples/ws-echo-server.ts +++ b/context/effect/socket/examples/ws-echo-server.ts @@ -1,9 +1,9 @@ import { NodeRuntime } from '@effect/platform-node' import { layerWebSocket } from '@effect/platform-node/NodeSocketServer' -import type { CloseEvent, Socket as SocketType } from '@effect/platform/Socket' -import { toChannelString } from '@effect/platform/Socket' -import type { Address } from '@effect/platform/SocketServer' -import { SocketServer } from '@effect/platform/SocketServer' +import type { CloseEvent, Socket as SocketType } from 'effect/unstable/socket/Socket' +import { toChannelString } from 'effect/unstable/socket/Socket' +import type { Address } from 'effect/unstable/socket/SocketServer' +import { SocketServer } from 'effect/unstable/socket/SocketServer' import { Effect, Stream } from 'effect' /** diff --git a/context/effect/socket/examples/ws-json-client.ts b/context/effect/socket/examples/ws-json-client.ts index 0a222c7d54..1b6f5b2b23 100644 --- a/context/effect/socket/examples/ws-json-client.ts +++ b/context/effect/socket/examples/ws-json-client.ts @@ -1,12 +1,12 @@ import { NodeRuntime } from '@effect/platform-node' -import type { Socket as SocketType } from '@effect/platform/Socket' +import { Duration, Effect, Fiber, Schema, Stream } from 'effect' +import type { Socket as SocketType } from 'effect/unstable/socket/Socket' import { CloseEvent, layerWebSocketConstructorGlobal, makeWebSocket, toChannelString, -} from '@effect/platform/Socket' -import { Duration, Effect, Fiber, Schema, Stream } from 'effect' +} from 'effect/unstable/socket/Socket' /** * Example: WebSocket JSON client with schema validation. @@ -26,19 +26,19 @@ const socketTextStream = (socket: SocketType) => ) /** Tagged union for client -> server messages. */ -const ClientMessageSchema = Schema.Union( +const ClientMessageSchema = Schema.Union([ Schema.TaggedStruct('ping', { id: Schema.String, }), Schema.TaggedStruct('echo', { text: Schema.String, }), -) +]) type ClientMessage = typeof ClientMessageSchema.Type /** Tagged union for server -> client responses. */ -const ServerMessageSchema = Schema.Union( +const ServerMessageSchema = Schema.Union([ Schema.TaggedStruct('pong', { id: Schema.String, receivedAt: Schema.Number, @@ -46,20 +46,20 @@ const ServerMessageSchema = Schema.Union( Schema.TaggedStruct('echoed', { text: Schema.String, }), -) +]) type ServerMessage = typeof ServerMessageSchema.Type /** Encode a typed client message to JSON. */ const encodeClientMessage = Effect.fn('ws-json.encode')(function* (message: ClientMessage) { - return yield* Schema.encode(Schema.parseJson(ClientMessageSchema))(message) + return yield* Schema.encode(Schema.fromJsonString(ClientMessageSchema))(message) }) /** Decode a JSON string into a typed server response. */ const decodeServerMessage = Effect.fn('ws-json.decode')(function* (raw: string) { - const message: ServerMessage = yield* Schema.decodeUnknown(Schema.parseJson(ServerMessageSchema))( - raw, - ) + const message: ServerMessage = yield* Schema.decodeUnknown( + Schema.fromJsonString(ServerMessageSchema), + )(raw) return message }) diff --git a/context/effect/socket/examples/ws-json-server.ts b/context/effect/socket/examples/ws-json-server.ts index b8918f545d..cc90dc3cd3 100644 --- a/context/effect/socket/examples/ws-json-server.ts +++ b/context/effect/socket/examples/ws-json-server.ts @@ -1,10 +1,10 @@ import { NodeRuntime } from '@effect/platform-node' import { layerWebSocket } from '@effect/platform-node/NodeSocketServer' -import type { CloseEvent, Socket as SocketType } from '@effect/platform/Socket' -import { toChannelString } from '@effect/platform/Socket' -import type { Address } from '@effect/platform/SocketServer' -import { SocketServer } from '@effect/platform/SocketServer' import { Effect, Schema, Stream } from 'effect' +import type { CloseEvent, Socket as SocketType } from 'effect/unstable/socket/Socket' +import { toChannelString } from 'effect/unstable/socket/Socket' +import type { Address } from 'effect/unstable/socket/SocketServer' +import { SocketServer } from 'effect/unstable/socket/SocketServer' /** * Example: WebSocket JSON server with schema validation. @@ -15,29 +15,29 @@ import { Effect, Schema, Stream } from 'effect' * - error logging on invalid payloads */ /** Error surfaced when the client payload does not match the schema. */ -class InvalidClientMessageError extends Schema.TaggedError()( +class InvalidClientMessageError extends Schema.TaggedErrorClass()( 'InvalidClientMessageError', { - cause: Schema.Defect, + cause: Schema.Defect(), message: Schema.String, raw: Schema.String, }, ) {} /** Tagged union for client -> server messages. */ -const ClientMessageSchema = Schema.Union( +const ClientMessageSchema = Schema.Union([ Schema.TaggedStruct('ping', { id: Schema.String, }), Schema.TaggedStruct('echo', { text: Schema.String, }), -) +]) type ClientMessage = typeof ClientMessageSchema.Type /** Tagged union for server -> client responses. */ -const ServerMessageSchema = Schema.Union( +const ServerMessageSchema = Schema.Union([ Schema.TaggedStruct('pong', { id: Schema.String, receivedAt: Schema.Number, @@ -45,13 +45,13 @@ const ServerMessageSchema = Schema.Union( Schema.TaggedStruct('echoed', { text: Schema.String, }), -) +]) type ServerMessage = typeof ServerMessageSchema.Type /** Decode a JSON string into a typed client message. */ const decodeClientMessage = Effect.fn('ws-json.decode')(function* (raw: string) { - return yield* Schema.decodeUnknown(Schema.parseJson(ClientMessageSchema))(raw).pipe( + return yield* Schema.decodeUnknown(Schema.fromJsonString(ClientMessageSchema))(raw).pipe( Effect.map((message) => { const decoded: ClientMessage = message return decoded @@ -69,7 +69,7 @@ const decodeClientMessage = Effect.fn('ws-json.decode')(function* (raw: string) /** Encode a typed server message to JSON. */ const encodeServerMessage = Effect.fn('ws-json.encode')(function* (message: ServerMessage) { - return yield* Schema.encode(Schema.parseJson(ServerMessageSchema))(message) + return yield* Schema.encode(Schema.fromJsonString(ServerMessageSchema))(message) }) /** Normalize socket address for logs. */ diff --git a/context/effect/socket/package.json b/context/effect/socket/package.json index 8a1158d680..7231dbee75 100644 --- a/context/effect/socket/package.json +++ b/context/effect/socket/package.json @@ -3,10 +3,8 @@ "private": true, "type": "module", "dependencies": { - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "effect": "3.21.4" + "@effect/platform-node": "4.0.0-beta.102", + "effect": "4.0.0-beta.102" }, "devDependencies": { "@types/node": "26.0.0" diff --git a/context/effect/socket/package.json.genie.ts b/context/effect/socket/package.json.genie.ts index 2b5602a929..721bb84cf1 100644 --- a/context/effect/socket/package.json.genie.ts +++ b/context/effect/socket/package.json.genie.ts @@ -9,7 +9,7 @@ import { const composition = catalog.compose({ workspace: workspaceMember({ memberPath: 'context/effect/socket' }), dependencies: { - external: catalog.pick('@effect/platform', '@effect/platform-node', '@effect/rpc', 'effect'), + external: catalog.pick('effect', '@effect/platform-node'), }, devDependencies: { external: catalog.pick('@types/node'), diff --git a/context/opentui/README.md b/context/opentui/README.md index ff54cba263..180a7bb3d0 100644 --- a/context/opentui/README.md +++ b/context/opentui/README.md @@ -181,12 +181,12 @@ renderer.keyInput.on('keypress', (key: { name: string; ctrl: boolean }) => { ## Effect Atoms Integration -For complex state, use `@effect-atom/atom` with OpenTUI: +For complex state, use `effect/unstable/reactivity` with OpenTUI: ```tsx -import { Registry } from '@effect-atom/atom' -import { RegistryContext, useAtomValue } from '@effect-atom/atom-react' -import { Atom } from '@effect-atom/atom' +import { AtomRegistry } from 'effect/unstable/reactivity' +import { RegistryContext, useAtomValue } from '@effect/atom-react' +import { Atom } from 'effect/unstable/reactivity' const countAtom = Atom.make(0) @@ -198,7 +198,7 @@ const App = () => { const main = async () => { const renderer = await createCliRenderer() const root = createRoot(renderer) - const registry = Registry.make() + const registry = AtomRegistry.make() // CRITICAL: Subscribe to atoms to keep them alive registry.subscribe(countAtom, () => {}) diff --git a/context/opentui/examples/effect-atoms-keyboard.tsx b/context/opentui/examples/effect-atoms-keyboard.tsx index c4408070de..c6ea9f9337 100644 --- a/context/opentui/examples/effect-atoms-keyboard.tsx +++ b/context/opentui/examples/effect-atoms-keyboard.tsx @@ -9,8 +9,8 @@ * * Run: bun examples/effect-atoms-keyboard.tsx */ -import { Atom, Registry } from '@effect-atom/atom' -import { RegistryContext, useAtomValue } from '@effect-atom/atom-react' +import { Atom, AtomRegistry } from 'effect/unstable/reactivity' +import { RegistryContext, useAtomValue } from '@effect/atom-react' import { createCliRenderer } from '@opentui/core' import { createRoot } from '@opentui/react' import { Cause, Effect, Fiber, Layer, ManagedRuntime, Runtime } from 'effect' @@ -122,7 +122,7 @@ const App = () => { const main = async () => { const renderer = await createCliRenderer({ exitOnCtrlC: false }) const root = createRoot(renderer) - const registry = Registry.make() + const registry = AtomRegistry.make() // Initialize Effect runtime from Layer const managedRuntime = ManagedRuntime.make(AppLayer) diff --git a/context/opentui/package.json b/context/opentui/package.json index 87b7bccb60..1ca7a2d902 100644 --- a/context/opentui/package.json +++ b/context/opentui/package.json @@ -3,25 +3,16 @@ "private": true, "type": "module", "dependencies": { - "@effect-atom/atom": "0.5.3", - "@effect-atom/atom-react": "0.5.0", + "@effect/atom-react": "4.0.0-beta.102", "@opentui/core": "0.4.1", "@opentui/react": "0.4.1", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7" }, "devDependencies": { "@types/node": "26.0.0", "@types/react": "19.2.17" }, - "pnpm": { - "overrides": { - "effect": "3.21.4", - "@effect/platform": "0.96.2", - "@effect/experimental": "0.60.0", - "@effect/rpc": "0.75.1" - } - }, "$genie": { "source": "package.json.genie.ts", "warning": "DO NOT EDIT - changes will be overwritten", diff --git a/context/opentui/package.json.genie.ts b/context/opentui/package.json.genie.ts index c07e3a96d1..fc0d925b97 100644 --- a/context/opentui/package.json.genie.ts +++ b/context/opentui/package.json.genie.ts @@ -9,14 +9,7 @@ import { const composition = catalog.compose({ workspace: workspaceMember({ memberPath: 'context/opentui' }), dependencies: { - external: catalog.pick( - '@effect-atom/atom', - '@effect-atom/atom-react', - '@opentui/core', - '@opentui/react', - 'effect', - 'react', - ), + external: catalog.pick('@effect/atom-react', '@opentui/core', '@opentui/react', 'effect', 'react'), }, devDependencies: { external: catalog.pick('@types/node', '@types/react'), @@ -28,15 +21,6 @@ export default packageJson( name: 'opentui-examples', private: true, type: 'module', - pnpm: { - overrides: { - // Force version alignment for Effect ecosystem packages - // @effect-atom/atom brings in older versions as transitive deps - // TODO: Remove once new @effect-atom/atom is released - // https://github.com/tim-smart/effect-atom/issues/401 - ...catalog.pick('effect', '@effect/platform', '@effect/experimental', '@effect/rpc'), - }, - }, } satisfies PackageJsonData, composition, ) diff --git a/devenv.nix b/devenv.nix index 1ea1d469eb..f00e5c1b41 100644 --- a/devenv.nix +++ b/devenv.nix @@ -93,10 +93,11 @@ let name = "mr"; entry = "packages/@overeng/megarepo/bin/mr.ts"; }; - ciToolsSourceCli = mkSourceCli { - name = "ci-tools"; - entry = "packages/@overeng/ci-tools/bin/ci-tools.ts"; - }; + # LIVE-MIGRATION BRIDGE effect-3-4 B8 — DELETE at contraction after Phase 4 repairs ci-tools/otel-contract and notion-cli/notion-md to Effect 4 — https://github.com/overengineeringstudio/effect-utils/pull/1019 + preFlipEffectUtils = builtins.getFlake "github:overengineeringstudio/effect-utils/49c45f197c056b40d993da5e5847029d5e0d9bfb"; + ciToolsSourceCli = preFlipEffectUtils.packages.${currentSystem}.ci-tools; + notionCliBridgePkg = preFlipEffectUtils.packages.${currentSystem}.notion-cli; + # LIVE-MIGRATION END effect-3-4 # CLI packages built with Nix (for hash management) nixCliPackages = [ @@ -405,7 +406,11 @@ in # Workflow reports run as standalone CI control-plane steps, including when # a deploy is skipped. Use the hermetic package instead of relying on an # ambient source-workspace node_modules projection. - (taskModules.workflow-report { }) + (taskModules.workflow-report { + # LIVE-MIGRATION BRIDGE effect-3-4 B8 — DELETE at contraction after Phase 4 repairs ci-tools/otel-contract and notion-cli/notion-md to Effect 4 — https://github.com/overengineeringstudio/effect-utils/pull/1019 + ciToolsBin = "${ciToolsSourceCli}/bin/ci-tools"; + # LIVE-MIGRATION END effect-3-4 + }) (taskModules.lint-oxc { oxlintPkg = oxlintWithPlugins; lintPaths = [ @@ -487,8 +492,9 @@ in pkgs.flock # Cross-process locking for setup tasks (see setup.nix) # restate-server (+ restate CLI) on $PATH for restate-effect integration tests. restate - # Use the packaged wrapper so `notion db ...` runs on Node 24 with node:sqlite. - repoFlake.packages.${currentSystem}.notion-cli + # LIVE-MIGRATION BRIDGE effect-3-4 B8 — DELETE at contraction after Phase 4 repairs ci-tools/otel-contract and notion-cli/notion-md to Effect 4 — https://github.com/overengineeringstudio/effect-utils/pull/1019 + notionCliBridgePkg + # LIVE-MIGRATION END effect-3-4 # Rust binaries on PATH for local smoke tests and downstream wrappers. repoFlake.packages.${currentSystem}.otelite repoFlake.packages.${currentSystem}.otel-scrape diff --git a/genie/external.ts b/genie/external.ts index 4996daebbf..a0551acc20 100644 --- a/genie/external.ts +++ b/genie/external.ts @@ -272,21 +272,10 @@ export const catalog = defineCatalog({ '@standard-schema/spec': '1.1.0', // Effect ecosystem - '@effect/ai': '0.36.0', - effect: '3.21.4', - '@effect/platform': '0.96.2', - '@effect/platform-node': '0.107.0', - '@effect/cli': '0.75.2', - '@effect/vitest': '0.29.0', - '@effect/printer': '0.49.0', - '@effect/printer-ansi': '0.49.0', - '@effect/typeclass': '0.40.0', - '@effect/cluster': '0.59.0', - '@effect/sql': '0.51.1', - '@effect/experimental': '0.60.0', - '@effect/workflow': '0.18.2', - '@effect/rpc': '0.75.1', - '@effect/opentelemetry': '0.63.0', + effect: '4.0.0-beta.102', + '@effect/platform-node': '4.0.0-beta.102', + '@effect/vitest': '4.0.0-beta.102', + '@effect/opentelemetry': '4.0.0-beta.102', // React ecosystem react: '19.2.7', @@ -377,8 +366,7 @@ export const catalog = defineCatalog({ 'is-dom': '1.1.0', // OpenTUI / Effect Atom (experimental) - '@effect-atom/atom': '0.5.3', - '@effect-atom/atom-react': '0.5.0', + '@effect/atom-react': '4.0.0-beta.102', '@opentui/core': '0.4.1', '@opentui/react': '0.4.1', @@ -428,11 +416,7 @@ export const commonPnpmPolicySettings = { dedupePeerDependents: true as const, strictPeerDependencies: true as const, peerDependencyRules: { - /** @effect-atom/atom@0.5.3 pins pre-1.0 Effect peer ranges that don't cover our versions */ allowedVersions: { - '@effect/experimental': '>=0.58.0', - '@effect/platform': '>=0.94.2', - '@effect/rpc': '>=0.73.0', eslint: '>=10.0.0', typescript: '>=6.0.0', vitest: '>=4.0.0', @@ -443,10 +427,10 @@ export const commonPnpmPolicySettings = { verifyStoreIntegrity: true as const, strictStorePkgContentCheck: true as const, ignoreScripts: true as const, - // This dependency refresh intentionally tracks the newest Effect 3 line and + // This dependency refresh intentionally tracks the Effect 4 beta line and // Node 26 types. Keep minimum-release-age strict globally, but allow these // reviewed packages to advance immediately as part of the coordinated upgrade. - minimumReleaseAgeExclude: ['@effect/platform', '@types/node', 'effect'], + minimumReleaseAgeExclude: ['@effect/platform-node', '@types/node', 'effect'], pmOnFail: 'ignore' as const, /** Disable until pnpm#10393 is resolved (install no-ops for workspace changes) */ optimisticRepeatInstall: false as const, @@ -552,13 +536,12 @@ export const createEffectUtilsRefs = (basePath: string) => export const utilsPatches = definePatchedDependencies({ location: 'packages/@overeng/utils', patches: { - /* Restrict `http.client` tracer span attribute emission to a small - allowlist of response headers. Upstream hardcodes emission of every - header, which for chatty APIs (Notion: ~31 headers per response) - dumps low-signal attrs like cf-ray / alt-svc / cookie flags into - every span. Keeps the observability-critical ones - (content-type, x-notion-request-id, retry-after, ...). */ - '@effect/platform@0.96.2': './patches/@effect__platform@0.96.0.patch', + /* LIVE-MIGRATION BRIDGE effect-3-4 B3 — DELETE at contraction — https://github.com/Effect-TS/effect/pull/6697 + Effect 4 beta.102 emits every redacted request/response header name as an + HTTP client span attribute. Preserve the v3 telemetry allowlist until + upstream exposes HttpClient.TracerHeaderFilter and we adopt that beta. */ + 'effect@4.0.0-beta.102': './patches/effect@4.0.0-beta.102.patch', + /* LIVE-MIGRATION END effect-3-4 */ }, }) diff --git a/genie/internal.ts b/genie/internal.ts index b3bc479310..9d7f360f95 100644 --- a/genie/internal.ts +++ b/genie/internal.ts @@ -160,11 +160,7 @@ export const commonPnpmWorkspaceData = { patchedDependencies: { ...effectUtilsWorkspacePatches }, allowUnusedPatches: true as const, peerDependencyRules: { - /** @effect-atom/atom@0.5.3 pins pre-1.0 Effect peer ranges that don't cover our versions */ allowedVersions: { - '@effect/experimental': '>=0.58.0', - '@effect/platform': '>=0.94.2', - '@effect/rpc': '>=0.73.0', eslint: '>=10.0.0', typescript: '>=6.0.0', vitest: '>=4.0.0', diff --git a/nix/oxc-config-plugin.nix b/nix/oxc-config-plugin.nix index 46a7557c6f..33af49e4c9 100644 --- a/nix/oxc-config-plugin.nix +++ b/nix/oxc-config-plugin.nix @@ -29,7 +29,7 @@ let pnpm = pinnedPnpm; }; packageDir = "packages/@overeng/oxc-config"; - pnpmDepsHash = "sha256-0EpX7HQHfu4XyZjO/0N4QjOKaFXNB3F/iEUzGesURjM="; + pnpmDepsHash = "sha256-UY6mLb26sY3NWsB1Sg4HgQOrrNufBvmiZpsm5ABY048="; srcPath = if builtins.isAttrs src && builtins.hasAttr "outPath" src then diff --git a/packages/@overeng/agent-session-ingest/package.json b/packages/@overeng/agent-session-ingest/package.json index 0d093f544a..79a678305e 100644 --- a/packages/@overeng/agent-session-ingest/package.json +++ b/packages/@overeng/agent-session-ingest/package.json @@ -21,18 +21,16 @@ } }, "devDependencies": { - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/vitest": "0.29.0", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "@effect/platform": "^0.96.2", - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/agent-session-ingest/package.json.genie.ts b/packages/@overeng/agent-session-ingest/package.json.genie.ts index 622d565835..f10baec1a2 100644 --- a/packages/@overeng/agent-session-ingest/package.json.genie.ts +++ b/packages/@overeng/agent-session-ingest/package.json.genie.ts @@ -9,21 +9,14 @@ import { } from '../../../genie/internal.ts' import utilsDevPkg from '../utils-dev/package.json.genie.ts' -const peerDepNames = ['@effect/platform', 'effect'] as const +const peerDepNames = ['effect'] as const const workspaceDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/agent-session-ingest' }), devDependencies: { workspace: [utilsDevPkg], external: { - ...catalog.pick( - ...peerDepNames, - '@effect/platform-node', - '@effect/vitest', - '@types/node', - 'typescript', - 'vitest', - ), + ...catalog.pick(...peerDepNames, '@effect/platform-node', '@effect/vitest', '@types/node', 'typescript', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/agent-session-ingest/src/adapters.integration-test-helpers.ts b/packages/@overeng/agent-session-ingest/src/adapters.integration-test-helpers.ts index d6e1956676..a6000b41df 100644 --- a/packages/@overeng/agent-session-ingest/src/adapters.integration-test-helpers.ts +++ b/packages/@overeng/agent-session-ingest/src/adapters.integration-test-helpers.ts @@ -1,31 +1,31 @@ import { appendFile } from 'node:fs/promises' import * as nodePath from 'node:path' -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' import { Effect, Schema } from 'effect' import { expect } from 'vitest' import type { SessionSourceAdapter } from './schema/core.ts' /** Raised when a test helper fails to append records to a JSONL artifact. */ -export class JsonlArtifactAppendError extends Schema.TaggedError()( +export class JsonlArtifactAppendError extends Schema.TaggedErrorClass()( 'JsonlArtifactAppendError', { message: Schema.String, path: Schema.String, - cause: Schema.Defect, + cause: Schema.Defect(), }, ) {} /** Schema-based JSON encoder for test fixtures (mirrors `JSON.stringify`). */ -const encodeJson = Schema.encodeSync(Schema.parseJson()) +const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)) /** Encodes a value to its JSON string for test fixtures via Effect Schema. */ export const stringifyJson = (value: unknown): string => encodeJson(value) /** Shared Node runtime layer for adapter integration tests. */ -export const TestLayer = NodeContext.layer +export const TestLayer = NodeServices.layer /** Ensures the adapter discovers exactly one artifact and returns it. */ export const expectSingleArtifact = (adapter: SessionSourceAdapter) => diff --git a/packages/@overeng/agent-session-ingest/src/adapters/claude.ts b/packages/@overeng/agent-session-ingest/src/adapters/claude.ts index 59e62c64c3..0ce6d55003 100644 --- a/packages/@overeng/agent-session-ingest/src/adapters/claude.ts +++ b/packages/@overeng/agent-session-ingest/src/adapters/claude.ts @@ -1,8 +1,8 @@ import { lstatSync } from 'node:fs' import * as nodePath from 'node:path' -import { FileSystem } from '@effect/platform' import { Effect, Option, Schema } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { SessionSourceDiscoveryError } from '../errors.ts' import type { SessionSourceAdapter } from '../schema/core.ts' @@ -11,18 +11,18 @@ import { makeAppendOnlyJsonlAdapter } from './jsonl.ts' const QueueOperationRecord = Schema.Struct({ type: Schema.Literal('queue-operation'), - operation: Schema.Literal('enqueue', 'dequeue'), + operation: Schema.Literals(['enqueue', 'dequeue']), timestamp: Schema.DateTimeUtc, sessionId: Schema.String, content: Schema.optional(Schema.String), -}).annotations({ identifier: 'AgentSessionIngest.ClaudeQueueOperationRecord' }) +}).annotate({ identifier: 'AgentSessionIngest.ClaudeQueueOperationRecord' }) const HookProgressData = Schema.Struct({ type: Schema.String, hookEvent: Schema.optional(Schema.String), hookName: Schema.optional(Schema.String), command: Schema.optional(Schema.String), -}).annotations({ identifier: 'AgentSessionIngest.ClaudeHookProgressData' }) +}).annotate({ identifier: 'AgentSessionIngest.ClaudeHookProgressData' }) const ProgressRecord = Schema.Struct({ type: Schema.Literal('progress'), @@ -40,7 +40,7 @@ const ProgressRecord = Schema.Struct({ uuid: Schema.String, timestamp: Schema.DateTimeUtc, data: HookProgressData, -}).annotations({ identifier: 'AgentSessionIngest.ClaudeProgressRecord' }) +}).annotate({ identifier: 'AgentSessionIngest.ClaudeProgressRecord' }) const ClaudeTextBlock = Schema.Struct({ type: Schema.Literal('text'), @@ -84,7 +84,7 @@ const ClaudeGenericContentBlock = Schema.Struct({ }) /** Content block types in Claude assistant message responses. */ -export const ClaudeAssistantContentBlock = Schema.Union( +export const ClaudeAssistantContentBlock = Schema.Union([ ClaudeTextBlock, ClaudeThinkingBlock, ClaudeToolUseBlock, @@ -92,11 +92,11 @@ export const ClaudeAssistantContentBlock = Schema.Union( ClaudeServerToolUseBlock, ClaudeServerToolResultBlock, ClaudeGenericContentBlock, -).annotations({ identifier: 'AgentSessionIngest.ClaudeAssistantContentBlock' }) +]).annotate({ identifier: 'AgentSessionIngest.ClaudeAssistantContentBlock' }) export type ClaudeAssistantContentBlock = typeof ClaudeAssistantContentBlock.Type /** Content block types in Claude user messages (tool results and text). */ -export const ClaudeUserContentBlock = Schema.Union( +export const ClaudeUserContentBlock = Schema.Union([ Schema.Struct({ type: Schema.Literal('tool_result'), tool_use_id: Schema.String, @@ -108,12 +108,12 @@ export const ClaudeUserContentBlock = Schema.Union( text: Schema.String, }), ClaudeGenericContentBlock, -).annotations({ identifier: 'AgentSessionIngest.ClaudeUserContentBlock' }) +]).annotate({ identifier: 'AgentSessionIngest.ClaudeUserContentBlock' }) const MessageEnvelope = Schema.Struct({ role: Schema.String, content: Schema.Unknown, -}).annotations({ identifier: 'AgentSessionIngest.ClaudeMessageEnvelope' }) +}).annotate({ identifier: 'AgentSessionIngest.ClaudeMessageEnvelope' }) const UserRecord = Schema.Struct({ type: Schema.Literal('user'), @@ -132,7 +132,7 @@ const UserRecord = Schema.Struct({ sourceToolAssistantUUID: Schema.optional(Schema.String), toolUseResult: Schema.optional(Schema.Unknown), message: MessageEnvelope, -}).annotations({ identifier: 'AgentSessionIngest.ClaudeUserRecord' }) +}).annotate({ identifier: 'AgentSessionIngest.ClaudeUserRecord' }) const AssistantMessagePayload = Schema.Struct({ model: Schema.optional(Schema.String), @@ -143,7 +143,7 @@ const AssistantMessagePayload = Schema.Struct({ stop_reason: Schema.optional(Schema.NullOr(Schema.String)), stop_sequence: Schema.optional(Schema.NullOr(Schema.String)), usage: Schema.optional(Schema.Unknown), -}).annotations({ identifier: 'AgentSessionIngest.ClaudeAssistantMessagePayload' }) +}).annotate({ identifier: 'AgentSessionIngest.ClaudeAssistantMessagePayload' }) const AssistantRecord = Schema.Struct({ type: Schema.Literal('assistant'), @@ -160,7 +160,7 @@ const AssistantRecord = Schema.Struct({ uuid: Schema.String, timestamp: Schema.DateTimeUtc, message: AssistantMessagePayload, -}).annotations({ identifier: 'AgentSessionIngest.ClaudeAssistantRecord' }) +}).annotate({ identifier: 'AgentSessionIngest.ClaudeAssistantRecord' }) const SystemRecord = Schema.Struct({ type: Schema.Literal('system'), @@ -173,13 +173,13 @@ const SystemRecord = Schema.Struct({ uuid: Schema.String, timestamp: Schema.DateTimeUtc, content: Schema.Unknown, -}).annotations({ identifier: 'AgentSessionIngest.ClaudeSystemRecord' }) +}).annotate({ identifier: 'AgentSessionIngest.ClaudeSystemRecord' }) const GenericClaudeRecord = Schema.Struct({ type: Schema.String, sessionId: Schema.optional(Schema.String), timestamp: Schema.optional(Schema.DateTimeUtc), -}).annotations({ identifier: 'AgentSessionIngest.ClaudeGenericRecord' }) +}).annotate({ identifier: 'AgentSessionIngest.ClaudeGenericRecord' }) /** * Source-of-truth record union for Claude project/subagent transcript JSONL artifacts. @@ -188,14 +188,14 @@ const GenericClaudeRecord = Schema.Struct({ * - Native transcript store: `~/.claude/projects/(nested path).jsonl` * - Common shared-store target: `~/.claude-shared/projects/(nested path).jsonl` */ -export const ClaudeSessionRecord = Schema.Union( +export const ClaudeSessionRecord = Schema.Union([ QueueOperationRecord, ProgressRecord, UserRecord, AssistantRecord, SystemRecord, GenericClaudeRecord, -).annotations({ identifier: 'AgentSessionIngest.ClaudeSessionRecord' }) +]).annotate({ identifier: 'AgentSessionIngest.ClaudeSessionRecord' }) export type ClaudeSessionRecord = typeof ClaudeSessionRecord.Type const listClaudeJsonlFiles = Effect.fn('AgentSessionIngest.Claude.listClaudeJsonlFiles')( diff --git a/packages/@overeng/agent-session-ingest/src/adapters/codex.ts b/packages/@overeng/agent-session-ingest/src/adapters/codex.ts index bf4fcef602..ac5e61f165 100644 --- a/packages/@overeng/agent-session-ingest/src/adapters/codex.ts +++ b/packages/@overeng/agent-session-ingest/src/adapters/codex.ts @@ -1,8 +1,8 @@ import { lstatSync } from 'node:fs' import * as nodePath from 'node:path' -import { FileSystem } from '@effect/platform' import { Effect, Option, Schema } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { SessionSourceDiscoveryError } from '../errors.ts' import type { SessionSourceAdapter } from '../schema/core.ts' @@ -10,7 +10,7 @@ import { ArtifactDescriptor, SourceId } from '../schema/core.ts' import { makeAppendOnlyJsonlAdapter } from './jsonl.ts' const TextPart = Schema.Struct({ - type: Schema.Literal('input_text', 'output_text'), + type: Schema.Literals(['input_text', 'output_text']), text: Schema.String, }) @@ -23,8 +23,8 @@ const InputImagePart = Schema.Struct({ const MessageResponsePayload = Schema.Struct({ type: Schema.Literal('message'), - role: Schema.Literal('assistant', 'developer', 'system', 'user'), - content: Schema.Array(Schema.Union(TextPart, InputImagePart)), + role: Schema.Literals(['assistant', 'developer', 'system', 'user']), + content: Schema.Array(Schema.Union([TextPart, InputImagePart])), }) const ReasoningSummaryPart = Schema.Struct({ @@ -116,7 +116,7 @@ const TokenCountInfo = Schema.Struct({ * - Native transcript store: `~/.codex-/sessions/(nested path).jsonl` * - Discovery index: `state_5.sqlite` / `threads.rollout_path` */ -export const CodexSessionRecord = Schema.Union( +export const CodexSessionRecord = Schema.Union([ Schema.Struct({ timestamp: Schema.DateTimeUtc, type: Schema.Literal('session_meta'), @@ -160,7 +160,7 @@ export const CodexSessionRecord = Schema.Union( LegacySessionMetaRecord, LegacyStateRecord, LegacyTopLevelRecord, -).annotations({ identifier: 'AgentSessionIngest.CodexSessionRecord' }) +]).annotate({ identifier: 'AgentSessionIngest.CodexSessionRecord' }) export type CodexSessionRecord = typeof CodexSessionRecord.Type /** @@ -174,7 +174,7 @@ export const CodexSessionIndexEntry = Schema.Struct({ id: Schema.String, thread_name: Schema.String, updated_at: Schema.String, -}).annotations({ identifier: 'AgentSessionIngest.CodexSessionIndexEntry' }) +}).annotate({ identifier: 'AgentSessionIngest.CodexSessionIndexEntry' }) export type CodexSessionIndexEntry = typeof CodexSessionIndexEntry.Type const listJsonlFiles = Effect.fn('AgentSessionIngest.Codex.listJsonlFiles')( diff --git a/packages/@overeng/agent-session-ingest/src/adapters/jsonl.ts b/packages/@overeng/agent-session-ingest/src/adapters/jsonl.ts index 0c9db34601..e8af9b7403 100644 --- a/packages/@overeng/agent-session-ingest/src/adapters/jsonl.ts +++ b/packages/@overeng/agent-session-ingest/src/adapters/jsonl.ts @@ -1,4 +1,4 @@ -import type { FileSystem } from '@effect/platform' +import type { FileSystem } from 'effect/FileSystem' import { Effect, Schema } from 'effect' import type { SessionSourceDiscoveryError } from '../errors.ts' @@ -68,7 +68,7 @@ export const makeAppendOnlyJsonlAdapter = ( }) const records = yield* Effect.forEach(splitCompleteJsonlRecords(read.text), (line) => - Schema.decodeUnknown(Schema.parseJson(options.recordSchema))(line).pipe( + Schema.decodeUnknown(Schema.fromJsonString(options.recordSchema))(line).pipe( Effect.mapError( (cause) => new SessionArtifactDecodeError({ diff --git a/packages/@overeng/agent-session-ingest/src/adapters/opencode.ts b/packages/@overeng/agent-session-ingest/src/adapters/opencode.ts index d6df3e5b72..6d08d42eb2 100644 --- a/packages/@overeng/agent-session-ingest/src/adapters/opencode.ts +++ b/packages/@overeng/agent-session-ingest/src/adapters/opencode.ts @@ -16,31 +16,31 @@ const OpenCodeSessionRow = Schema.Struct({ directory: Schema.String, title: Schema.String, version: Schema.String, - time_created: Schema.NonNegativeInt, - time_updated: Schema.NonNegativeInt, - time_archived: Schema.optional(Schema.NullOr(Schema.NonNegativeInt)), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodeSessionRow' }) + time_created: Schema.Natural, + time_updated: Schema.Natural, + time_archived: Schema.optional(Schema.NullOr(Schema.Natural)), +}).annotate({ identifier: 'AgentSessionIngest.OpenCodeSessionRow' }) const OpenCodeSessionDiscoveryRow = Schema.Struct({ id: Schema.String, - time_archived: Schema.NullOr(Schema.NonNegativeInt), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodeSessionDiscoveryRow' }) + time_archived: Schema.NullOr(Schema.Natural), +}).annotate({ identifier: 'AgentSessionIngest.OpenCodeSessionDiscoveryRow' }) const OpenCodeMessageRow = Schema.Struct({ id: Schema.String, session_id: Schema.String, - time_created: Schema.NonNegativeInt, - time_updated: Schema.NonNegativeInt, + time_created: Schema.Natural, + time_updated: Schema.Natural, data: Schema.Unknown, -}).annotations({ identifier: 'AgentSessionIngest.OpenCodeMessageRow' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodeMessageRow' }) const OpenCodePartRow = Schema.Struct({ id: Schema.String, session_id: Schema.String, - time_created: Schema.NonNegativeInt, - time_updated: Schema.NonNegativeInt, + time_created: Schema.Natural, + time_updated: Schema.Natural, data: Schema.Unknown, -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartRow' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartRow' }) const OpenCodeMessageData = Schema.Struct({ role: Schema.String, @@ -57,15 +57,15 @@ const OpenCodeMessageData = Schema.Struct({ }), ), summary: Schema.optional(Schema.Unknown), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodeMessageData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodeMessageData' }) const OpenCodeMessageRecord = Schema.TaggedStruct('OpenCodeMessage', { id: Schema.String, sessionId: Schema.String, - timeCreated: Schema.NonNegativeInt, - timeUpdated: Schema.NonNegativeInt, + timeCreated: Schema.Natural, + timeUpdated: Schema.Natural, data: OpenCodeMessageData, -}).annotations({ identifier: 'AgentSessionIngest.OpenCodeMessageRecord' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodeMessageRecord' }) const OpenCodePartToolData = Schema.Struct({ type: Schema.Literal('tool'), @@ -79,23 +79,23 @@ const OpenCodePartToolData = Schema.Struct({ metadata: Schema.optional(Schema.Unknown), time: Schema.optional(Schema.Unknown), }), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartToolData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartToolData' }) const OpenCodePartTextData = Schema.Struct({ type: Schema.Literal('text'), text: Schema.String, -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartTextData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartTextData' }) const OpenCodePartReasoningData = Schema.Struct({ type: Schema.Literal('reasoning'), text: Schema.String, metadata: Schema.optional(Schema.Unknown), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartReasoningData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartReasoningData' }) const OpenCodePartStepStartData = Schema.Struct({ type: Schema.Literal('step-start'), text: Schema.optional(Schema.String), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartStepStartData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartStepStartData' }) const OpenCodePartStepFinishData = Schema.Struct({ type: Schema.Literal('step-finish'), @@ -103,35 +103,35 @@ const OpenCodePartStepFinishData = Schema.Struct({ snapshot: Schema.optional(Schema.String), cost: Schema.optional(Schema.Number), tokens: Schema.optional(Schema.Unknown), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartStepFinishData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartStepFinishData' }) const OpenCodePartPatchData = Schema.Struct({ type: Schema.Literal('patch'), patch: Schema.optional(Schema.String), metadata: Schema.optional(Schema.Unknown), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartPatchData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartPatchData' }) const OpenCodePartFileData = Schema.Struct({ type: Schema.Literal('file'), path: Schema.optional(Schema.String), metadata: Schema.optional(Schema.Unknown), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartFileData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartFileData' }) const OpenCodePartSubtaskData = Schema.Struct({ type: Schema.Literal('subtask'), metadata: Schema.optional(Schema.Unknown), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartSubtaskData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartSubtaskData' }) const OpenCodePartCompactionData = Schema.Struct({ type: Schema.Literal('compaction'), metadata: Schema.optional(Schema.Unknown), -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartCompactionData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartCompactionData' }) const OpenCodePartGenericData = Schema.Struct({ type: Schema.String, -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartGenericData' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartGenericData' }) -const OpenCodePartData = Schema.Union( +const OpenCodePartData = Schema.Union([ OpenCodePartToolData, OpenCodePartTextData, OpenCodePartReasoningData, @@ -142,19 +142,19 @@ const OpenCodePartData = Schema.Union( OpenCodePartSubtaskData, OpenCodePartCompactionData, OpenCodePartGenericData, -).annotations({ identifier: 'AgentSessionIngest.OpenCodePartData' }) +]).annotate({ identifier: 'AgentSessionIngest.OpenCodePartData' }) const OpenCodePartRecord = Schema.TaggedStruct('OpenCodePart', { id: Schema.String, sessionId: Schema.String, - timeCreated: Schema.NonNegativeInt, - timeUpdated: Schema.NonNegativeInt, + timeCreated: Schema.Natural, + timeUpdated: Schema.Natural, data: OpenCodePartData, -}).annotations({ identifier: 'AgentSessionIngest.OpenCodePartRecord' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodePartRecord' }) const OpenCodeSessionRecord = Schema.TaggedStruct('OpenCodeSession', { session: OpenCodeSessionRow, -}).annotations({ identifier: 'AgentSessionIngest.OpenCodeSessionRecord' }) +}).annotate({ identifier: 'AgentSessionIngest.OpenCodeSessionRecord' }) /** * Source-of-truth record union for OpenCode native SQLite session storage. @@ -163,11 +163,11 @@ const OpenCodeSessionRecord = Schema.TaggedStruct('OpenCodeSession', { * - Database: `~/.local/share/opencode/opencode.db` * - Tables: `session`, `message`, `part` */ -export const OpenCodeRecord = Schema.Union( +export const OpenCodeRecord = Schema.Union([ OpenCodeSessionRecord, OpenCodeMessageRecord, OpenCodePartRecord, -).annotations({ identifier: 'AgentSessionIngest.OpenCodeRecord' }) +]).annotate({ identifier: 'AgentSessionIngest.OpenCodeRecord' }) export type OpenCodeRecord = typeof OpenCodeRecord.Type type SqliteValue = string | number | bigint | Uint8Array | null @@ -335,7 +335,7 @@ const parseOpenCodeRowData = Effect.fn('AgentSessionIngest.OpenCode.parseOpenCod readonly rawData: unknown readonly message: string }) => - Schema.decodeUnknown(Schema.parseJson(Schema.Unknown))(String(options.rawData)).pipe( + Schema.decodeUnknown(Schema.fromJsonString(Schema.Unknown))(String(options.rawData)).pipe( Effect.mapError( (cause) => new SessionArtifactDecodeError({ diff --git a/packages/@overeng/agent-session-ingest/src/claude.integration.test.ts b/packages/@overeng/agent-session-ingest/src/claude.integration.test.ts index 77422836de..8e0185afba 100644 --- a/packages/@overeng/agent-session-ingest/src/claude.integration.test.ts +++ b/packages/@overeng/agent-session-ingest/src/claude.integration.test.ts @@ -1,4 +1,4 @@ -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { expect } from 'vitest' diff --git a/packages/@overeng/agent-session-ingest/src/codex.integration.test.ts b/packages/@overeng/agent-session-ingest/src/codex.integration.test.ts index 551483be5c..ab39a07b21 100644 --- a/packages/@overeng/agent-session-ingest/src/codex.integration.test.ts +++ b/packages/@overeng/agent-session-ingest/src/codex.integration.test.ts @@ -1,7 +1,7 @@ import { utimesSync } from 'node:fs' import * as nodePath from 'node:path' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { expect } from 'vitest' diff --git a/packages/@overeng/agent-session-ingest/src/errors.ts b/packages/@overeng/agent-session-ingest/src/errors.ts index a0e6053da6..0dc7ec7309 100644 --- a/packages/@overeng/agent-session-ingest/src/errors.ts +++ b/packages/@overeng/agent-session-ingest/src/errors.ts @@ -1,54 +1,54 @@ import { Schema } from 'effect' /** Raised when session artifact bytes cannot be read from disk. */ -export class SessionArtifactReadError extends Schema.TaggedError()( +export class SessionArtifactReadError extends Schema.TaggedErrorClass()( 'SessionArtifactReadError', { message: Schema.String, path: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** Raised when a source record cannot be decoded into its source-of-truth schema. */ -export class SessionArtifactDecodeError extends Schema.TaggedError()( +export class SessionArtifactDecodeError extends Schema.TaggedErrorClass()( 'SessionArtifactDecodeError', { message: Schema.String, sourceId: Schema.String, artifactId: Schema.String, rawRecord: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** Raised when persisted checkpoint state cannot be decoded back into structured data. */ -export class SessionCheckpointDecodeError extends Schema.TaggedError()( +export class SessionCheckpointDecodeError extends Schema.TaggedErrorClass()( 'SessionCheckpointDecodeError', { message: Schema.String, path: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** Raised when checkpoint state cannot be encoded or written back to disk. */ -export class SessionCheckpointWriteError extends Schema.TaggedError()( +export class SessionCheckpointWriteError extends Schema.TaggedErrorClass()( 'SessionCheckpointWriteError', { message: Schema.String, path: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** Raised when source artifact discovery fails before ingestion can begin. */ -export class SessionSourceDiscoveryError extends Schema.TaggedError()( +export class SessionSourceDiscoveryError extends Schema.TaggedErrorClass()( 'SessionSourceDiscoveryError', { message: Schema.String, sourceId: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} diff --git a/packages/@overeng/agent-session-ingest/src/files/append-only.ts b/packages/@overeng/agent-session-ingest/src/files/append-only.ts index b90edd3393..8bad33322b 100644 --- a/packages/@overeng/agent-session-ingest/src/files/append-only.ts +++ b/packages/@overeng/agent-session-ingest/src/files/append-only.ts @@ -1,6 +1,6 @@ import { open as openFile } from 'node:fs/promises' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option, Schema } from 'effect' import { SessionArtifactReadError } from '../errors.ts' diff --git a/packages/@overeng/agent-session-ingest/src/files/content-version.ts b/packages/@overeng/agent-session-ingest/src/files/content-version.ts index 5b84a628cf..b61939142a 100644 --- a/packages/@overeng/agent-session-ingest/src/files/content-version.ts +++ b/packages/@overeng/agent-session-ingest/src/files/content-version.ts @@ -1,6 +1,6 @@ import { open as openFile } from 'node:fs/promises' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option, Schema } from 'effect' import { SessionArtifactReadError } from '../errors.ts' diff --git a/packages/@overeng/agent-session-ingest/src/files/mutable.ts b/packages/@overeng/agent-session-ingest/src/files/mutable.ts index efe3720479..fb6b0c4a76 100644 --- a/packages/@overeng/agent-session-ingest/src/files/mutable.ts +++ b/packages/@overeng/agent-session-ingest/src/files/mutable.ts @@ -1,6 +1,6 @@ import { open as openFile } from 'node:fs/promises' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { SessionArtifactReadError } from '../errors.ts' diff --git a/packages/@overeng/agent-session-ingest/src/ingest.unit.test.ts b/packages/@overeng/agent-session-ingest/src/ingest.unit.test.ts index 0949c83485..f3480edcfd 100644 --- a/packages/@overeng/agent-session-ingest/src/ingest.unit.test.ts +++ b/packages/@overeng/agent-session-ingest/src/ingest.unit.test.ts @@ -1,7 +1,7 @@ import * as nodePath from 'node:path' -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { Effect } from 'effect' import { expect } from 'vitest' @@ -10,7 +10,7 @@ import { Vitest } from '@overeng/utils-dev/node-vitest' import { readAppendOnlyTextFileSince } from './files/append-only.ts' import { readMutableTextFileIfChanged } from './files/mutable.ts' -const TestLayer = NodeContext.layer +const TestLayer = NodeServices.layer Vitest.describe('agent-session-ingest file readers', () => { Vitest.it.effect('resets append-only offsets after truncation', () => diff --git a/packages/@overeng/agent-session-ingest/src/normalized/schema.ts b/packages/@overeng/agent-session-ingest/src/normalized/schema.ts index 3c10cae8ff..eb2e2341ba 100644 --- a/packages/@overeng/agent-session-ingest/src/normalized/schema.ts +++ b/packages/@overeng/agent-session-ingest/src/normalized/schema.ts @@ -10,7 +10,7 @@ export const SessionMeta = Schema.TaggedStruct('SessionMeta', { gitBranch: Schema.optional(Schema.String), tool: Schema.optional(Schema.String), timestamp: Schema.optional(Schema.DateTimeUtc), -}).annotations({ identifier: 'AgentSessionIngest.Normalized.SessionMeta' }) +}).annotate({ identifier: 'AgentSessionIngest.Normalized.SessionMeta' }) /** User-authored message content. */ export const UserMessage = Schema.TaggedStruct('UserMessage', { @@ -19,7 +19,7 @@ export const UserMessage = Schema.TaggedStruct('UserMessage', { sessionId: Schema.optional(Schema.String), content: Schema.String, timestamp: Schema.DateTimeUtc, -}).annotations({ identifier: 'AgentSessionIngest.Normalized.UserMessage' }) +}).annotate({ identifier: 'AgentSessionIngest.Normalized.UserMessage' }) /** Text content from an assistant response. */ export const AssistantText = Schema.TaggedStruct('AssistantText', { @@ -29,7 +29,7 @@ export const AssistantText = Schema.TaggedStruct('AssistantText', { content: Schema.String, model: Schema.optional(Schema.String), timestamp: Schema.DateTimeUtc, -}).annotations({ identifier: 'AgentSessionIngest.Normalized.AssistantText' }) +}).annotate({ identifier: 'AgentSessionIngest.Normalized.AssistantText' }) /** Internal reasoning/thinking content from an assistant. */ export const Thinking = Schema.TaggedStruct('Thinking', { @@ -38,7 +38,7 @@ export const Thinking = Schema.TaggedStruct('Thinking', { sessionId: Schema.optional(Schema.String), content: Schema.String, timestamp: Schema.DateTimeUtc, -}).annotations({ identifier: 'AgentSessionIngest.Normalized.Thinking' }) +}).annotate({ identifier: 'AgentSessionIngest.Normalized.Thinking' }) /** Tool invocation with input parameters. */ export const ToolCallStart = Schema.TaggedStruct('ToolCallStart', { @@ -50,7 +50,7 @@ export const ToolCallStart = Schema.TaggedStruct('ToolCallStart', { serverName: Schema.optional(Schema.String), input: Schema.Unknown, timestamp: Schema.DateTimeUtc, -}).annotations({ identifier: 'AgentSessionIngest.Normalized.ToolCallStart' }) +}).annotate({ identifier: 'AgentSessionIngest.Normalized.ToolCallStart' }) /** Tool completion with output and error status. */ export const ToolCallEnd = Schema.TaggedStruct('ToolCallEnd', { @@ -61,18 +61,18 @@ export const ToolCallEnd = Schema.TaggedStruct('ToolCallEnd', { output: Schema.Unknown, isError: Schema.optional(Schema.Boolean), timestamp: Schema.DateTimeUtc, -}).annotations({ identifier: 'AgentSessionIngest.Normalized.ToolCallEnd' }) +}).annotate({ identifier: 'AgentSessionIngest.Normalized.ToolCallEnd' }) /** Step start/finish marker (OpenCode-specific). */ export const StepBoundary = Schema.TaggedStruct('StepBoundary', { sourceId: Schema.String, sessionId: Schema.optional(Schema.String), - kind: Schema.Literal('start', 'finish'), + kind: Schema.Literals(['start', 'finish']), cost: Schema.optional(Schema.Number), tokens: Schema.optional(Schema.Unknown), reason: Schema.optional(Schema.String), timestamp: Schema.DateTimeUtc, -}).annotations({ identifier: 'AgentSessionIngest.Normalized.StepBoundary' }) +}).annotate({ identifier: 'AgentSessionIngest.Normalized.StepBoundary' }) /** System-level message content. */ export const SystemMessage = Schema.TaggedStruct('SystemMessage', { @@ -80,7 +80,7 @@ export const SystemMessage = Schema.TaggedStruct('SystemMessage', { sessionId: Schema.optional(Schema.String), content: Schema.Unknown, timestamp: Schema.DateTimeUtc, -}).annotations({ identifier: 'AgentSessionIngest.Normalized.SystemMessage' }) +}).annotate({ identifier: 'AgentSessionIngest.Normalized.SystemMessage' }) /** Catch-all for provider-specific events with no semantic equivalent. */ export const GenericEvent = Schema.TaggedStruct('GenericEvent', { @@ -89,10 +89,10 @@ export const GenericEvent = Schema.TaggedStruct('GenericEvent', { eventType: Schema.String, data: Schema.Unknown, timestamp: Schema.optional(Schema.DateTimeUtc), -}).annotations({ identifier: 'AgentSessionIngest.Normalized.GenericEvent' }) +}).annotate({ identifier: 'AgentSessionIngest.Normalized.GenericEvent' }) /** Provider-agnostic union of all normalized record types. */ -export const NormalizedRecord = Schema.Union( +export const NormalizedRecord = Schema.Union([ SessionMeta, UserMessage, AssistantText, @@ -102,5 +102,5 @@ export const NormalizedRecord = Schema.Union( StepBoundary, SystemMessage, GenericEvent, -).annotations({ identifier: 'AgentSessionIngest.NormalizedRecord' }) +]).annotate({ identifier: 'AgentSessionIngest.NormalizedRecord' }) export type NormalizedRecord = typeof NormalizedRecord.Type diff --git a/packages/@overeng/agent-session-ingest/src/opencode.integration.test.ts b/packages/@overeng/agent-session-ingest/src/opencode.integration.test.ts index 386406249d..a5ee892484 100644 --- a/packages/@overeng/agent-session-ingest/src/opencode.integration.test.ts +++ b/packages/@overeng/agent-session-ingest/src/opencode.integration.test.ts @@ -1,7 +1,7 @@ import * as nodePath from 'node:path' import { DatabaseSync } from 'node:sqlite' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { expect } from 'vitest' diff --git a/packages/@overeng/agent-session-ingest/src/schema/core.ts b/packages/@overeng/agent-session-ingest/src/schema/core.ts index 04e37cc489..c421cbe188 100644 --- a/packages/@overeng/agent-session-ingest/src/schema/core.ts +++ b/packages/@overeng/agent-session-ingest/src/schema/core.ts @@ -1,34 +1,34 @@ -import type { FileSystem } from '@effect/platform' import type { Effect } from 'effect' import { Schema } from 'effect' +import type { FileSystem } from 'effect/FileSystem' import type { SessionIngestError, SessionSourceDiscoveryError } from '../errors.ts' /** Stable logical identifier for a session source such as `codex`. */ export const SourceId = Schema.String.pipe( - Schema.minLength(1), + Schema.check(Schema.isMinLength(1)), Schema.brand('SourceId'), - Schema.annotations({ identifier: 'AgentSessionIngest.SourceId' }), + Schema.annotate({ identifier: 'AgentSessionIngest.SourceId' }), ) export type SourceId = typeof SourceId.Type /** Stable per-source identifier for one discoverable session artifact. */ export const ArtifactId = Schema.String.pipe( - Schema.minLength(1), + Schema.check(Schema.isMinLength(1)), Schema.brand('ArtifactId'), - Schema.annotations({ identifier: 'AgentSessionIngest.ArtifactId' }), + Schema.annotate({ identifier: 'AgentSessionIngest.ArtifactId' }), ) export type ArtifactId = typeof ArtifactId.Type /** Filesystem path to a discovered source artifact. */ export const ArtifactPath = Schema.String.pipe( - Schema.minLength(1), - Schema.annotations({ identifier: 'AgentSessionIngest.ArtifactPath' }), + Schema.check(Schema.isMinLength(1)), + Schema.annotate({ identifier: 'AgentSessionIngest.ArtifactPath' }), ) export type ArtifactPath = typeof ArtifactPath.Type /** Artifact lifecycle classification used to decide how aggressively to reprocess it. */ -export const ArtifactStatus = Schema.Literal('open', 'stable', 'finalized').annotations({ +export const ArtifactStatus = Schema.Literals(['open', 'stable', 'finalized']).annotate({ identifier: 'AgentSessionIngest.ArtifactStatus', }) export type ArtifactStatus = typeof ArtifactStatus.Type @@ -39,45 +39,45 @@ export const ArtifactDescriptor = Schema.Struct({ artifactId: ArtifactId, path: ArtifactPath, status: ArtifactStatus, -}).annotations({ identifier: 'AgentSessionIngest.ArtifactDescriptor' }) +}).annotate({ identifier: 'AgentSessionIngest.ArtifactDescriptor' }) export type ArtifactDescriptor = typeof ArtifactDescriptor.Type /** Compact content signature used to detect artifact growth, rewrites, or truncation. */ export const ContentVersion = Schema.Struct({ - sizeBytes: Schema.NonNegativeInt, - modifiedAtEpochMs: Schema.NonNegativeInt, - headHash: Schema.optional(Schema.NonEmptyTrimmedString), - tailHash: Schema.NonEmptyTrimmedString, -}).annotations({ identifier: 'AgentSessionIngest.ContentVersion' }) + sizeBytes: Schema.Natural, + modifiedAtEpochMs: Schema.Natural, + headHash: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), + tailHash: Schema.Trimmed.check(Schema.isNonEmpty()), +}).annotate({ identifier: 'AgentSessionIngest.ContentVersion' }) export type ContentVersion = typeof ContentVersion.Type /** Cursor for append-only artifacts that can be resumed from a byte offset. */ export const AppendOnlyCursor = Schema.TaggedStruct('AppendOnlyCursor', { - offsetBytes: Schema.NonNegativeInt, + offsetBytes: Schema.Natural, contentVersion: ContentVersion, -}).annotations({ identifier: 'AgentSessionIngest.AppendOnlyCursor' }) +}).annotate({ identifier: 'AgentSessionIngest.AppendOnlyCursor' }) export type AppendOnlyCursor = typeof AppendOnlyCursor.Type /** Cursor for mutable artifacts that are re-read when the content signature changes. */ export const ContentVersionCursor = Schema.TaggedStruct('ContentVersionCursor', { contentVersion: ContentVersion, -}).annotations({ identifier: 'AgentSessionIngest.ContentVersionCursor' }) +}).annotate({ identifier: 'AgentSessionIngest.ContentVersionCursor' }) export type ContentVersionCursor = typeof ContentVersionCursor.Type /** Cursor for ordered mutable artifacts that support incremental replay via an update watermark. */ export const UpdatedAtCursor = Schema.TaggedStruct('UpdatedAtCursor', { - updatedAtEpochMs: Schema.NonNegativeInt, + updatedAtEpochMs: Schema.Natural, lastRecordKey: Schema.optional(Schema.String), contentVersion: ContentVersion, -}).annotations({ identifier: 'AgentSessionIngest.UpdatedAtCursor' }) +}).annotate({ identifier: 'AgentSessionIngest.UpdatedAtCursor' }) export type UpdatedAtCursor = typeof UpdatedAtCursor.Type /** Unified cursor union used by checkpoint persistence. */ -export const ArtifactCursor = Schema.Union( +export const ArtifactCursor = Schema.Union([ AppendOnlyCursor, ContentVersionCursor, UpdatedAtCursor, -).annotations({ identifier: 'AgentSessionIngest.ArtifactCursor' }) +]).annotate({ identifier: 'AgentSessionIngest.ArtifactCursor' }) export type ArtifactCursor = typeof ArtifactCursor.Type /** Persisted checkpoint entry for one source artifact. */ @@ -87,12 +87,12 @@ export const IngestionCheckpoint = Schema.Struct({ path: ArtifactPath, status: ArtifactStatus, cursor: ArtifactCursor, - updatedAtEpochMs: Schema.NonNegativeInt, -}).annotations({ identifier: 'AgentSessionIngest.IngestionCheckpoint' }) + updatedAtEpochMs: Schema.Natural, +}).annotate({ identifier: 'AgentSessionIngest.IngestionCheckpoint' }) export type IngestionCheckpoint = typeof IngestionCheckpoint.Type /** JSONL codec used for checkpoint files on disk. */ -export const IngestionCheckpointJsonLine = Schema.parseJson(IngestionCheckpoint).annotations({ +export const IngestionCheckpointJsonLine = Schema.fromJsonString(IngestionCheckpoint).annotate({ identifier: 'AgentSessionIngest.IngestionCheckpointJsonLine', }) diff --git a/packages/@overeng/agent-session-ingest/src/services.integration.test.ts b/packages/@overeng/agent-session-ingest/src/services.integration.test.ts index fc1bd473de..6843e02f9b 100644 --- a/packages/@overeng/agent-session-ingest/src/services.integration.test.ts +++ b/packages/@overeng/agent-session-ingest/src/services.integration.test.ts @@ -1,5 +1,5 @@ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { Layer, Ref, Schema, Effect } from 'effect' import { expect } from 'vitest' @@ -117,7 +117,7 @@ Vitest.describe('agent-session-ingest services', () => { } yield* ingestSource(adapter).pipe( - Effect.provide(Layer.mergeAll(NodeContext.layer, checkpointLayer)), + Effect.provide(Layer.mergeAll(NodeServices.layer, checkpointLayer)), ) const saved = yield* Ref.get(savedRef) @@ -189,7 +189,7 @@ Vitest.describe('checkpoint store wire baselines (cross-major invariant)', () => expect(stringifyJson(loaded)).toMatchInlineSnapshot( `"[{"sourceId":"codex","artifactId":"2026/07/28/rollout","path":"/var/lib/agent/sessions/2026/07/28/rollout.jsonl","status":"stable","cursor":{"_tag":"AppendOnlyCursor","offsetBytes":321,"contentVersion":{"sizeBytes":654,"modifiedAtEpochMs":1785225600123,"headHash":"fnv1a:00000000","tailHash":"fnv1a:ffffffff"}},"updatedAtEpochMs":1785225600456},{"sourceId":"opencode","artifactId":"thread:世界","path":"/var/lib/agent/opencode.db","status":"open","cursor":{"_tag":"UpdatedAtCursor","updatedAtEpochMs":1785225600789,"lastRecordKey":"","contentVersion":{"sizeBytes":9007199254740991,"modifiedAtEpochMs":1785225600999,"tailHash":"fnv1a:résumé"}},"updatedAtEpochMs":1785225601111}]"`, ) - }).pipe(Effect.scoped, Effect.provide(NodeContext.layer)), + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ) Vitest.it.effect('captures checkpoint decode failures as stable JSON', () => @@ -219,6 +219,6 @@ Vitest.describe('checkpoint store wire baselines (cross-major invariant)', () => `"{"_tag":"SessionCheckpointDecodeError","message":"Failed to decode checkpoint entry"}"`, ) } - }).pipe(Effect.scoped, Effect.provide(NodeContext.layer)), + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ) }) diff --git a/packages/@overeng/agent-session-ingest/src/services/CheckpointStore.ts b/packages/@overeng/agent-session-ingest/src/services/CheckpointStore.ts index ffad70e3b7..fbd85b5e3d 100644 --- a/packages/@overeng/agent-session-ingest/src/services/CheckpointStore.ts +++ b/packages/@overeng/agent-session-ingest/src/services/CheckpointStore.ts @@ -1,6 +1,6 @@ import * as nodePath from 'node:path' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Context, Effect, Layer, Schema } from 'effect' import { SessionCheckpointDecodeError, SessionCheckpointWriteError } from '../errors.ts' @@ -13,7 +13,7 @@ export const buildCheckpointKey = ( ) => JSON.stringify([descriptor.sourceId, descriptor.artifactId] as const) /** Service for loading and saving deduped ingestion checkpoints. */ -export class CheckpointStore extends Context.Tag('AgentSessionIngest/CheckpointStore')< +export class CheckpointStore extends Context.Service< CheckpointStore, { readonly list: () => Effect.Effect< @@ -24,7 +24,7 @@ export class CheckpointStore extends Context.Tag('AgentSessionIngest/CheckpointS checkpoints: ReadonlyArray, ) => Effect.Effect } ->() {} +>()('AgentSessionIngest/CheckpointStore') {} /** File-backed checkpoint store for incremental source ingestion. */ export const makeFileCheckpointStore = (options: { path: string }) => diff --git a/packages/@overeng/agent-session-ingest/src/watch.ts b/packages/@overeng/agent-session-ingest/src/watch.ts index a43d41d74a..6906bf9d6c 100644 --- a/packages/@overeng/agent-session-ingest/src/watch.ts +++ b/packages/@overeng/agent-session-ingest/src/watch.ts @@ -1,4 +1,4 @@ -import type { FileSystem } from '@effect/platform' +import type { FileSystem } from 'effect/FileSystem' import { Schedule, Stream } from 'effect' import type { SessionIngestError, SessionSourceDiscoveryError } from './errors.ts' diff --git a/packages/@overeng/ci-tools/bin/ci-tools.ts b/packages/@overeng/ci-tools/bin/ci-tools.ts index 8dde31d754..fc42816aae 100644 --- a/packages/@overeng/ci-tools/bin/ci-tools.ts +++ b/packages/@overeng/ci-tools/bin/ci-tools.ts @@ -1,9 +1,9 @@ #!/usr/bin/env bun -import * as Cli from '@effect/cli' -import { FetchHttpClient } from '@effect/platform' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect, Layer, Schema } from 'effect' +import * as Cli from 'effect/unstable/cli' +import { FetchHttpClient } from 'effect/unstable/http' import { ServiceIdentity } from '@overeng/otel-contract' import { rewriteHelpSubcommand } from '@overeng/utils/node/cli-help-rewrite' @@ -35,7 +35,7 @@ const program = Effect.gen(function* () { CliVersion.enrichErrors, Effect.provideService(CliVersion, { name: 'ci-tools', version }), Effect.provide( - Layer.mergeAll(NodeContext.layer, withTelemetry({ identity, shape: 'cli', endpoint })), + Layer.mergeAll(NodeServices.layer, withTelemetry({ identity, shape: 'cli', endpoint })), ), ) }).pipe(Effect.provide(FetchHttpClient.layer)) diff --git a/packages/@overeng/ci-tools/nix/build.nix b/packages/@overeng/ci-tools/nix/build.nix index 102a0caac1..7c7a297dbc 100644 --- a/packages/@overeng/ci-tools/nix/build.nix +++ b/packages/@overeng/ci-tools/nix/build.nix @@ -19,7 +19,7 @@ let workspaceRoot = src; # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { - "." = mkSharedHash "sha256-KOHU2Wf4qIAeis7nZKdJRBA192SYTpgn+ztjCEbSVVk="; + "." = mkSharedHash "sha256-boSUHOgGC/g7yVestuw3etMH+EKQHA8SBWPtrNqkzVc="; }; smokeTestArgs = [ "--help" ]; inherit gitRev commitTs dirty; diff --git a/packages/@overeng/ci-tools/package.json b/packages/@overeng/ci-tools/package.json index 3aa60a32ec..9461566f54 100644 --- a/packages/@overeng/ci-tools/package.json +++ b/packages/@overeng/ci-tools/package.json @@ -22,28 +22,21 @@ "@overeng/utils": "workspace:^" }, "devDependencies": { - "@effect/cli": "0.75.2", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/vitest": "0.29.0", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils": "workspace:^", "@overeng/utils-dev": "workspace:^", "@types/bun": "1.3.14", "@types/node": "26.0.0", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "@effect/cli": "^0.75.2", - "@effect/cluster": "^0.59.0", - "@effect/experimental": "^0.60.0", - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/rpc": "^0.75.1", - "@effect/workflow": "^0.18.2", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@playwright/test": "^1.61.0", - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/ci-tools/package.json.genie.ts b/packages/@overeng/ci-tools/package.json.genie.ts index 60594f9909..ef85d2ec9f 100644 --- a/packages/@overeng/ci-tools/package.json.genie.ts +++ b/packages/@overeng/ci-tools/package.json.genie.ts @@ -18,20 +18,11 @@ const workspaceDeps = catalog.compose({ }, devDependencies: { workspace: [utilsDevPkg, utilsPkg], - external: catalog.pick( - '@effect/cli', - '@effect/platform', - '@effect/platform-node', - '@effect/vitest', - '@types/bun', - '@types/node', - 'typescript', - 'vitest', - ), + external: catalog.pick('effect', '@effect/platform-node', '@effect/vitest', '@types/bun', '@types/node', 'typescript', 'vitest'), }, peerDependencies: { workspace: [utilsPkg], - external: catalog.pick('@effect/cli'), + external: catalog.pick('effect'), }, }) diff --git a/packages/@overeng/ci-tools/src/cli-command.ts b/packages/@overeng/ci-tools/src/cli-command.ts index 63f3a532fa..8ac00e9ae2 100644 --- a/packages/@overeng/ci-tools/src/cli-command.ts +++ b/packages/@overeng/ci-tools/src/cli-command.ts @@ -3,8 +3,8 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { dirname } from 'node:path' -import { Command, Options } from '@effect/cli' import { Effect, Option } from 'effect' +import { Command, Flag as Options } from 'effect/unstable/cli' import { runNetlifyDeploy } from './deploy-netlify.ts' import { runVercelDeploy } from './deploy-vercel.ts' @@ -30,10 +30,10 @@ import { } from './quarantine.ts' const nonEmptyTextOption = (opts: { readonly name: string; readonly description: string }) => - Options.text(opts.name).pipe(Options.withDescription(opts.description)) + Options.string(opts.name).pipe(Options.withDescription(opts.description)) const optionalTextOption = (opts: { readonly name: string; readonly description: string }) => - Options.text(opts.name).pipe(Options.withDescription(opts.description), Options.withDefault('')) + Options.string(opts.name).pipe(Options.withDescription(opts.description), Options.withDefault('')) const optionalString = (value: string) => (value.length === 0 ? undefined : value) @@ -94,7 +94,7 @@ const collectBundleCommand = Command.make( name: 'output-path', description: 'Path that receives the encoded bundle JSON', }), - recordMarker: Options.text('record-marker').pipe( + recordMarker: Options.string('record-marker').pipe( Options.withDescription('Line marker prefix for workflow report records'), Options.withDefault(workflowReportRecordLineMarker), ), @@ -168,11 +168,11 @@ const renderCommentBodyCommand = Command.make( name: 'created-at-utc', description: 'Current report history entry timestamp, defaults to latest record timestamp', }), - timeZone: Options.text('time-zone').pipe( + timeZone: Options.string('time-zone').pipe( Options.withDescription('IANA time zone for rendered timestamps'), Options.withDefault('UTC'), ), - managedMarker: Options.text('managed-marker').pipe( + managedMarker: Options.string('managed-marker').pipe( Options.withDescription('Managed comment marker'), Options.withDefault(workflowReportManagedMarker), ), @@ -238,7 +238,7 @@ const findCommentCommand = Command.make( name: 'state-id', description: 'Stable managed state identifier', }), - managedMarker: Options.text('managed-marker').pipe( + managedMarker: Options.string('managed-marker').pipe( Options.withDescription('Managed comment marker'), Options.withDefault(workflowReportManagedMarker), ), @@ -274,7 +274,7 @@ const netlifyDeployCommand = Command.make( Options.withDescription('Netlify deploy mode'), Options.withDefault('draft' as const), ), - displayName: Options.text('display-name').pipe( + displayName: Options.string('display-name').pipe( Options.withDescription('Human-readable deploy target label'), Options.optional, ), @@ -282,47 +282,47 @@ const netlifyDeployCommand = Command.make( Options.withDescription('Pull request number for PR deploy mode'), Options.optional, ), - siteName: Options.text('site-name').pipe( + siteName: Options.string('site-name').pipe( Options.withDescription('Netlify site slug used for final alias URLs'), Options.optional, ), - siteIdEnv: Options.text('site-id-env').pipe( + siteIdEnv: Options.string('site-id-env').pipe( Options.withDescription('Environment variable containing the Netlify site id'), Options.withDefault('NETLIFY_SITE_ID'), ), - authTokenEnv: Options.text('auth-token-env').pipe( + authTokenEnv: Options.string('auth-token-env').pipe( Options.withDescription('Environment variable containing the Netlify auth token'), Options.withDefault('NETLIFY_AUTH_TOKEN'), ), - accountSlugEnv: Options.text('account-slug-env').pipe( + accountSlugEnv: Options.string('account-slug-env').pipe( Options.withDescription('Optional environment variable containing the Netlify account slug'), Options.optional, ), - workspaceFilter: Options.text('workspace-filter').pipe( + workspaceFilter: Options.string('workspace-filter').pipe( Options.withDescription('Optional Netlify monorepo workspace filter passed to the CLI'), Options.optional, ), - workflowReportOutputFile: Options.text('workflow-report-output-file').pipe( + workflowReportOutputFile: Options.string('workflow-report-output-file').pipe( Options.withDescription('Optional JSONL file that receives marked workflow-report records'), Options.optional, ), - githubOutputFile: Options.text('github-output-file').pipe( + githubOutputFile: Options.string('github-output-file').pipe( Options.withDescription('Optional GitHub Actions output file that receives deploy outputs'), Options.optional, ), - githubEnvFile: Options.text('github-env-file').pipe( + githubEnvFile: Options.string('github-env-file').pipe( Options.withDescription('Optional GitHub Actions env file that receives deploy env vars'), Options.optional, ), - urlEnvKey: Options.text('url-env-key').pipe( + urlEnvKey: Options.string('url-env-key').pipe( Options.withDescription('Optional env var name for the final deploy URL'), Options.optional, ), - netlifyBin: Options.text('netlify-bin').pipe( + netlifyBin: Options.string('netlify-bin').pipe( Options.withDescription('Netlify CLI binary path'), Options.withDefault('netlify'), ), - netlifyApiBaseUrl: Options.text('netlify-api-base-url').pipe( + netlifyApiBaseUrl: Options.string('netlify-api-base-url').pipe( Options.withDescription('Netlify API base URL'), Options.withDefault('https://api.netlify.com'), ), @@ -336,7 +336,7 @@ const netlifyDeployCommand = Command.make( ), Options.withDefault('fail' as const), ), - createdAtUtc: Options.text('created-at-utc').pipe( + createdAtUtc: Options.string('created-at-utc').pipe( Options.withDescription('Override record creation timestamp for deterministic tests'), Options.optional, ), @@ -344,15 +344,15 @@ const netlifyDeployCommand = Command.make( Options.withDescription('Enable shared-project live E2E alias guardrails'), Options.withDefault(false), ), - e2eReservedAliasPrefix: Options.text('e2e-reserved-alias-prefix').pipe( + e2eReservedAliasPrefix: Options.string('e2e-reserved-alias-prefix').pipe( Options.withDescription('Required alias prefix when shared-project E2E is enabled'), Options.withDefault('ci-tools-e2e'), ), - e2eVerifyPath: Options.text('e2e-verify-path').pipe( + e2eVerifyPath: Options.string('e2e-verify-path').pipe( Options.withDescription('Optional live E2E path to fetch after deploy'), Options.optional, ), - e2eVerifyText: Options.text('e2e-verify-text').pipe( + e2eVerifyText: Options.string('e2e-verify-text').pipe( Options.withDescription('Optional live E2E marker text expected at the verify path'), Options.optional, ), @@ -396,7 +396,7 @@ const vercelDeployCommand = Command.make( Options.withDescription('Vercel deploy mode'), Options.withDefault('preview' as const), ), - displayName: Options.text('display-name').pipe( + displayName: Options.string('display-name').pipe( Options.withDescription('Human-readable deploy target label'), Options.optional, ), @@ -404,57 +404,57 @@ const vercelDeployCommand = Command.make( Options.withDescription('Pull request number for PR deploy mode'), Options.optional, ), - aliasPrefix: Options.text('alias-prefix').pipe( + aliasPrefix: Options.string('alias-prefix').pipe( Options.withDescription('Optional Vercel alias prefix; defaults to target'), Options.optional, ), - aliasSuffix: Options.text('alias-suffix').pipe( + aliasSuffix: Options.string('alias-suffix').pipe( Options.withDescription('Optional suffix appended to Vercel aliases'), Options.optional, ), - productionDomain: Options.text('production-domain').pipe( + productionDomain: Options.string('production-domain').pipe( Options.withDescription('Production hostname to alias to the deployment (repeatable)'), Options.repeated, ), - projectIdEnv: Options.text('project-id-env').pipe( + projectIdEnv: Options.string('project-id-env').pipe( Options.withDescription('Environment variable containing the Vercel project id'), Options.withDefault('VERCEL_PROJECT_ID'), ), - orgIdEnv: Options.text('org-id-env').pipe( + orgIdEnv: Options.string('org-id-env').pipe( Options.withDescription('Environment variable containing the Vercel org/team id'), Options.withDefault('VERCEL_ORG_ID'), ), - authTokenEnv: Options.text('auth-token-env').pipe( + authTokenEnv: Options.string('auth-token-env').pipe( Options.withDescription('Environment variable containing the Vercel auth token'), Options.withDefault('VERCEL_TOKEN'), ), - teamIdEnv: Options.text('team-id-env').pipe( + teamIdEnv: Options.string('team-id-env').pipe( Options.withDescription('Optional environment variable containing the Vercel team id'), Options.optional, ), - scopeEnv: Options.text('scope-env').pipe( + scopeEnv: Options.string('scope-env').pipe( Options.withDescription('Optional environment variable containing the Vercel CLI scope slug'), Options.optional, ), - protectionBypassEnv: Options.text('protection-bypass-env').pipe( + protectionBypassEnv: Options.string('protection-bypass-env').pipe( Options.withDescription( 'Optional environment variable containing the Vercel protection bypass secret for live verification', ), Options.optional, ), - workflowReportOutputFile: Options.text('workflow-report-output-file').pipe( + workflowReportOutputFile: Options.string('workflow-report-output-file').pipe( Options.withDescription('Optional JSONL file that receives marked workflow-report records'), Options.optional, ), - githubOutputFile: Options.text('github-output-file').pipe( + githubOutputFile: Options.string('github-output-file').pipe( Options.withDescription('Optional GitHub Actions output file that receives deploy outputs'), Options.optional, ), - githubEnvFile: Options.text('github-env-file').pipe( + githubEnvFile: Options.string('github-env-file').pipe( Options.withDescription('Optional GitHub Actions env file that receives deploy env vars'), Options.optional, ), - urlEnvKey: Options.text('url-env-key').pipe( + urlEnvKey: Options.string('url-env-key').pipe( Options.withDescription('Optional env var name for the final deploy URL'), Options.optional, ), @@ -462,27 +462,27 @@ const vercelDeployCommand = Command.make( Options.withDescription('Run vercel pull/build before deploying a prebuilt-output artifact'), Options.withDefault(false), ), - vercelRootDirectory: Options.text('vercel-root-directory').pipe( + vercelRootDirectory: Options.string('vercel-root-directory').pipe( Options.withDescription( 'Optional Vercel project rootDirectory used while building prebuilt output', ), Options.optional, ), - buildEnv: Options.text('build-env').pipe( + buildEnv: Options.string('build-env').pipe( Options.withDescription( 'Environment variable for local vercel build (KEY=VALUE, repeatable)', ), Options.repeated, ), - vercelBin: Options.text('vercel-bin').pipe( + vercelBin: Options.string('vercel-bin').pipe( Options.withDescription('Vercel CLI binary path'), Options.withDefault('vercel'), ), - vercelApiBaseUrl: Options.text('vercel-api-base-url').pipe( + vercelApiBaseUrl: Options.string('vercel-api-base-url').pipe( Options.withDescription('Vercel API base URL'), Options.withDefault('https://api.vercel.com'), ), - createdAtUtc: Options.text('created-at-utc').pipe( + createdAtUtc: Options.string('created-at-utc').pipe( Options.withDescription('Override record creation timestamp for deterministic tests'), Options.optional, ), @@ -490,15 +490,15 @@ const vercelDeployCommand = Command.make( Options.withDescription('Enable shared-project live E2E alias guardrails'), Options.withDefault(false), ), - e2eReservedAliasPrefix: Options.text('e2e-reserved-alias-prefix').pipe( + e2eReservedAliasPrefix: Options.string('e2e-reserved-alias-prefix').pipe( Options.withDescription('Required alias prefix when shared-project E2E is enabled'), Options.withDefault('ci-tools-e2e'), ), - e2eVerifyPath: Options.text('e2e-verify-path').pipe( + e2eVerifyPath: Options.string('e2e-verify-path').pipe( Options.withDescription('Optional live E2E path to fetch after deploy'), Options.optional, ), - e2eVerifyText: Options.text('e2e-verify-text').pipe( + e2eVerifyText: Options.string('e2e-verify-text').pipe( Options.withDescription('Optional live E2E marker text expected at the verify path'), Options.optional, ), @@ -540,7 +540,7 @@ const quarantineValidateCommand = Command.make( name: 'ledger', description: 'Path to the quarantine ledger JSON', }), - today: Options.text('today').pipe( + today: Options.string('today').pipe( Options.withDescription( 'Evaluate expiry against this YYYY-MM-DD date instead of the current day', ), @@ -580,7 +580,7 @@ const quarantineAnnounceCommand = Command.make( name: 'label', description: 'Test target whose failure was tolerated; must match the entry target', }), - summaryFile: Options.text('summary-file').pipe( + summaryFile: Options.string('summary-file').pipe( Options.withDescription('Job summary file to append to (defaults to $GITHUB_STEP_SUMMARY)'), Options.withDefault(''), ), diff --git a/packages/@overeng/ci-tools/src/deploy-domain.ts b/packages/@overeng/ci-tools/src/deploy-domain.ts index 5155faf405..700ce5755a 100644 --- a/packages/@overeng/ci-tools/src/deploy-domain.ts +++ b/packages/@overeng/ci-tools/src/deploy-domain.ts @@ -11,27 +11,27 @@ import { } from './deploy-domain.contract.ts' import type { WorkflowReportRecord } from './mod.ts' -export const DeployProvider = Schema.Literal('netlify', 'vercel').annotations({ +export const DeployProvider = Schema.Literals(['netlify', 'vercel']).annotate({ identifier: 'CiTools.Deploy.Provider', }) export type DeployProvider = typeof DeployProvider.Type -export const DeployMode = Schema.Literal('prod', 'pr', 'draft', 'preview').annotations({ +export const DeployMode = Schema.Literals(['prod', 'pr', 'draft', 'preview']).annotate({ identifier: 'CiTools.Deploy.Mode', }) export type DeployMode = typeof DeployMode.Type -export const DeployStatus = Schema.Literal('success', 'failure', 'skipped').annotations({ +export const DeployStatus = Schema.Literals(['success', 'failure', 'skipped']).annotate({ identifier: 'CiTools.Deploy.Status', }) export type DeployStatus = typeof DeployStatus.Type -export const MissingAuthPolicy = Schema.Literal('fail', 'skip').annotations({ +export const MissingAuthPolicy = Schema.Literals(['fail', 'skip']).annotate({ identifier: 'CiTools.Deploy.MissingAuthPolicy', }) export type MissingAuthPolicy = typeof MissingAuthPolicy.Type -export const UnauthorizedPolicy = Schema.Literal('fail', 'skip').annotations({ +export const UnauthorizedPolicy = Schema.Literals(['fail', 'skip']).annotate({ identifier: 'CiTools.Deploy.UnauthorizedPolicy', }) export type UnauthorizedPolicy = typeof UnauthorizedPolicy.Type @@ -39,39 +39,39 @@ export type UnauthorizedPolicy = typeof UnauthorizedPolicy.Type export const PositiveInt = Schema.Number.pipe( Schema.int(), Schema.positive(), - Schema.annotations({ identifier: 'CiTools.Deploy.PositiveInt' }), + Schema.annotate({ identifier: 'CiTools.Deploy.PositiveInt' }), ) export type PositiveInt = typeof PositiveInt.Type -export const DeployTarget = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^[A-Za-z0-9._/-]+$/u), - Schema.annotations({ identifier: 'CiTools.Deploy.Target' }), +export const DeployTarget = Schema.Trimmed.check(Schema.isNonEmpty()).check( + Schema.check(Schema.isPattern(/^[A-Za-z0-9._/-]+$/u)), + Schema.annotate({ identifier: 'CiTools.Deploy.Target' }), ) export type DeployTarget = typeof DeployTarget.Type -export const DeployAlias = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^[a-z0-9][a-z0-9-]{0,62}$/u), - Schema.annotations({ identifier: 'CiTools.Deploy.Alias' }), +export const DeployAlias = Schema.Trimmed.check(Schema.isNonEmpty()).check( + Schema.check(Schema.isPattern(/^[a-z0-9][a-z0-9-]{0,62}$/u)), + Schema.annotate({ identifier: 'CiTools.Deploy.Alias' }), ) export type DeployAlias = typeof DeployAlias.Type -export const DeployHostname = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern( +export const DeployHostname = Schema.Trimmed.check(Schema.isNonEmpty()).check( + Schema.isPattern( /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/u, ), - Schema.annotations({ identifier: 'CiTools.Deploy.Hostname' }), + Schema.annotate({ identifier: 'CiTools.Deploy.Hostname' }), ) export type DeployHostname = typeof DeployHostname.Type -export const EnvVarName = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^[A-Z_][A-Z0-9_]*$/u), - Schema.annotations({ identifier: 'CiTools.Deploy.EnvVarName' }), +export const EnvVarName = Schema.Trimmed.check(Schema.isNonEmpty()).check( + Schema.check(Schema.isPattern(/^[A-Z_][A-Z0-9_]*$/u)), + Schema.annotate({ identifier: 'CiTools.Deploy.EnvVarName' }), ) export type EnvVarName = typeof EnvVarName.Type -export const RelativeHttpPath = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^\/(?!\/)/u), - Schema.annotations({ identifier: 'CiTools.Deploy.RelativeHttpPath' }), +export const RelativeHttpPath = Schema.Trimmed.check(Schema.isNonEmpty()).check( + Schema.check(Schema.isPattern(/^\/(?!\/)/u)), + Schema.annotate({ identifier: 'CiTools.Deploy.RelativeHttpPath' }), ) export type RelativeHttpPath = typeof RelativeHttpPath.Type @@ -79,14 +79,14 @@ export const HttpsUrl = Schema.URL.pipe( Schema.filter((url) => url.protocol === 'https:', { message: () => 'URL must use https:', }), - Schema.annotations({ identifier: 'CiTools.Deploy.HttpsUrl' }), + Schema.annotate({ identifier: 'CiTools.Deploy.HttpsUrl' }), ) export type HttpsUrl = typeof HttpsUrl.Type -export const DeployDiagnostic = Schema.Record({ - key: Schema.NonEmptyTrimmedString, - value: Schema.NonEmptyTrimmedString, -}).annotations({ identifier: 'CiTools.Deploy.Diagnostic' }) +export const DeployDiagnostic = Schema.Record( + Schema.Trimmed.check(Schema.isNonEmpty()), + Schema.Trimmed.check(Schema.isNonEmpty()), +).annotate({ identifier: 'CiTools.Deploy.Diagnostic' }) export type DeployDiagnostic = typeof DeployDiagnostic.Type export const NetlifyProviderConfig = Schema.TaggedStruct('NetlifyProviderConfig', { @@ -95,7 +95,7 @@ export const NetlifyProviderConfig = Schema.TaggedStruct('NetlifyProviderConfig' authTokenEnv: EnvVarName, accountSlugEnv: Schema.optional(EnvVarName), apiBaseUrlEnv: Schema.optional(EnvVarName), -}).annotations({ identifier: 'CiTools.Deploy.NetlifyProviderConfig' }) +}).annotate({ identifier: 'CiTools.Deploy.NetlifyProviderConfig' }) export type NetlifyProviderConfig = typeof NetlifyProviderConfig.Type export const VercelProviderConfig = Schema.TaggedStruct('VercelProviderConfig', { @@ -105,13 +105,13 @@ export const VercelProviderConfig = Schema.TaggedStruct('VercelProviderConfig', teamIdEnv: Schema.optional(EnvVarName), scopeEnv: Schema.optional(EnvVarName), protectionBypassEnv: Schema.optional(EnvVarName), -}).annotations({ identifier: 'CiTools.Deploy.VercelProviderConfig' }) +}).annotate({ identifier: 'CiTools.Deploy.VercelProviderConfig' }) export type VercelProviderConfig = typeof VercelProviderConfig.Type -export const DeployProviderConfig = Schema.Union( +export const DeployProviderConfig = Schema.Union([ NetlifyProviderConfig, VercelProviderConfig, -).annotations({ identifier: 'CiTools.Deploy.ProviderConfig' }) +]).annotate({ identifier: 'CiTools.Deploy.ProviderConfig' }) export type DeployProviderConfig = typeof DeployProviderConfig.Type export const DeployE2EConfig = Schema.TaggedStruct('DeployE2EConfig', { @@ -121,34 +121,34 @@ export const DeployE2EConfig = Schema.TaggedStruct('DeployE2EConfig', { verifyContent: Schema.optional( Schema.TaggedStruct('DeployVerifyContent', { path: RelativeHttpPath, - expectedText: Schema.NonEmptyTrimmedString, - }).annotations({ identifier: 'CiTools.Deploy.VerifyContent' }), + expectedText: Schema.Trimmed.check(Schema.isNonEmpty()), + }).annotate({ identifier: 'CiTools.Deploy.VerifyContent' }), ), -}).annotations({ identifier: 'CiTools.Deploy.E2EConfig' }) +}).annotate({ identifier: 'CiTools.Deploy.E2EConfig' }) export type DeployE2EConfig = typeof DeployE2EConfig.Type export const DeployInputV1 = Schema.TaggedStruct('DeployInput', { schemaVersion: Schema.Literal(1), provider: DeployProvider, target: DeployTarget, - displayName: Schema.optional(Schema.NonEmptyTrimmedString), + displayName: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), mode: DeployMode, - artifactDir: Schema.NonEmptyTrimmedString, + artifactDir: Schema.Trimmed.check(Schema.isNonEmpty()), alias: Schema.optional(DeployAlias), productionDomains: Schema.optional(Schema.Array(DeployHostname)), pr: Schema.optional(PositiveInt), - gitSha: Schema.optional(Schema.NonEmptyTrimmedString), - runId: Schema.optional(Schema.NonEmptyTrimmedString), - workflowReportOutputFile: Schema.optional(Schema.NonEmptyTrimmedString), + gitSha: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), + runId: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), + workflowReportOutputFile: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), e2e: Schema.optional(DeployE2EConfig), providerConfig: DeployProviderConfig, -}).annotations({ identifier: 'CiTools.Deploy.InputV1' }) +}).annotate({ identifier: 'CiTools.Deploy.InputV1' }) export type DeployInputV1 = typeof DeployInputV1.Type export const CleanupResult = Schema.TaggedStruct('CleanupResult', { - status: Schema.Literal('succeeded', 'failed', 'skipped'), - message: Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'CiTools.Deploy.CleanupResult' }) + status: Schema.Literals(['succeeded', 'failed', 'skipped']), + message: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'CiTools.Deploy.CleanupResult' }) export type CleanupResult = typeof CleanupResult.Type export const DeployResultV1 = Schema.TaggedStruct('DeployResult', { @@ -156,7 +156,7 @@ export const DeployResultV1 = Schema.TaggedStruct('DeployResult', { provider: DeployProvider, target: DeployTarget, mode: DeployMode, - deployId: Schema.optional(Schema.NonEmptyTrimmedString), + deployId: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), rawDeployUrl: HttpsUrl, finalUrl: HttpsUrl, alias: Schema.optional(DeployAlias), @@ -165,7 +165,7 @@ export const DeployResultV1 = Schema.TaggedStruct('DeployResult', { endedAtUtc: Schema.DateTimeUtc, attempts: PositiveInt, cleanup: Schema.optional(CleanupResult), -}).annotations({ identifier: 'CiTools.Deploy.ResultV1' }) +}).annotate({ identifier: 'CiTools.Deploy.ResultV1' }) export type DeployResultV1 = typeof DeployResultV1.Type const encodeDateTimeUtc = Schema.encodeSync(Schema.DateTimeUtc) @@ -204,16 +204,16 @@ export const deployTaskOutput = (opts: { readonly result: DeployResultV1 }) => { } export const deployTaskOutputLine = (opts: { readonly result: DeployResultV1 }) => - Schema.encodeSync(Schema.parseJson(Schema.Unknown))(deployTaskOutput(opts)) + Schema.encodeSync(Schema.fromJsonString(Schema.Unknown))(deployTaskOutput(opts)) const DeployFailureFields = { provider: DeployProvider, target: DeployTarget, - message: Schema.NonEmptyTrimmedString, + message: Schema.Trimmed.check(Schema.isNonEmpty()), diagnostics: Schema.optional(DeployDiagnostic), } as const -export class MissingAuth extends Schema.TaggedError( +export class MissingAuth extends Schema.TaggedErrorClass( '@overeng/ci-tools/deploy/MissingAuth', )('MissingAuth', { ...DeployFailureFields, @@ -224,7 +224,7 @@ export class MissingAuth extends Schema.TaggedError( } } -export class Unauthorized extends Schema.TaggedError( +export class Unauthorized extends Schema.TaggedErrorClass( '@overeng/ci-tools/deploy/Unauthorized', )('Unauthorized', { ...DeployFailureFields, @@ -234,18 +234,18 @@ export class Unauthorized extends Schema.TaggedError( } } -export class MissingBuildOutput extends Schema.TaggedError( +export class MissingBuildOutput extends Schema.TaggedErrorClass( '@overeng/ci-tools/deploy/MissingBuildOutput', )('MissingBuildOutput', { ...DeployFailureFields, - artifactDir: Schema.NonEmptyTrimmedString, + artifactDir: Schema.Trimmed.check(Schema.isNonEmpty()), }) { override get message(): string { return `missing build output for ${this.target}: ${this.artifactDir}` } } -export class ProviderProjectLookupFailed extends Schema.TaggedError( +export class ProviderProjectLookupFailed extends Schema.TaggedErrorClass( '@overeng/ci-tools/deploy/ProviderProjectLookupFailed', )('ProviderProjectLookupFailed', { ...DeployFailureFields, @@ -256,22 +256,29 @@ export class ProviderProjectLookupFailed extends Schema.TaggedError( +export class InvalidProviderOutput extends Schema.TaggedErrorClass( '@overeng/ci-tools/deploy/InvalidProviderOutput', )('InvalidProviderOutput', { ...DeployFailureFields, - outputKind: Schema.Literal('json', 'url', 'workflow-report-record', 'provider-response'), + outputKind: Schema.Literals(['json', 'url', 'workflow-report-record', 'provider-response']), }) { override get message(): string { return `${this.provider} returned invalid ${this.outputKind} output for ${this.target}` } } -export class ProviderOperationFailed extends Schema.TaggedError( +export class ProviderOperationFailed extends Schema.TaggedErrorClass( '@overeng/ci-tools/deploy/ProviderOperationFailed', )('ProviderOperationFailed', { ...DeployFailureFields, - operation: Schema.Literal('resolve-project', 'prepare', 'deploy', 'alias', 'verify', 'cleanup'), + operation: Schema.Literals([ + 'resolve-project', + 'prepare', + 'deploy', + 'alias', + 'verify', + 'cleanup', + ]), transient: Schema.Boolean, }) { override get message(): string { @@ -279,7 +286,7 @@ export class ProviderOperationFailed extends Schema.TaggedError( +export class UnsafeE2EAlias extends Schema.TaggedErrorClass( '@overeng/ci-tools/deploy/UnsafeE2EAlias', )('UnsafeE2EAlias', { ...DeployFailureFields, @@ -291,7 +298,7 @@ export class UnsafeE2EAlias extends Schema.TaggedError( } } -export class VerificationFailed extends Schema.TaggedError( +export class VerificationFailed extends Schema.TaggedErrorClass( '@overeng/ci-tools/deploy/VerificationFailed', )('VerificationFailed', { ...DeployFailureFields, @@ -303,7 +310,7 @@ export class VerificationFailed extends Schema.TaggedError( } } -export const DeployFailure = Schema.Union( +export const DeployFailure = Schema.Union([ MissingAuth, Unauthorized, MissingBuildOutput, @@ -312,7 +319,7 @@ export const DeployFailure = Schema.Union( ProviderOperationFailed, UnsafeE2EAlias, VerificationFailed, -).annotations({ identifier: 'CiTools.Deploy.Failure' }) +]).annotate({ identifier: 'CiTools.Deploy.Failure' }) export type DeployFailure = typeof DeployFailure.Type export const deployFailureRetryability = (failure: DeployFailure): boolean => { @@ -336,40 +343,40 @@ export type DeployRecordContext = { } const DeployWorkflowReportSubject = Schema.Struct({ - id: Schema.NonEmptyTrimmedString, - label: Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'CiTools.Deploy.WorkflowReportSubject' }) + id: Schema.Trimmed.check(Schema.isNonEmpty()), + label: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'CiTools.Deploy.WorkflowReportSubject' }) -const DeployWorkflowReportHttpsUrlString = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^https:\/\/[^\s]+$/u), - Schema.annotations({ identifier: 'CiTools.Deploy.WorkflowReportHttpsUrlString' }), +const DeployWorkflowReportHttpsUrlString = Schema.Trimmed.check(Schema.isNonEmpty()).check( + Schema.check(Schema.isPattern(/^https:\/\/[^\s]+$/u)), + Schema.annotate({ identifier: 'CiTools.Deploy.WorkflowReportHttpsUrlString' }), ) const DeployWorkflowReportLink = Schema.Struct({ - label: Schema.NonEmptyTrimmedString, + label: Schema.Trimmed.check(Schema.isNonEmpty()), url: DeployWorkflowReportHttpsUrlString, primary: Schema.optional(Schema.Boolean), -}).annotations({ identifier: 'CiTools.Deploy.WorkflowReportLink' }) +}).annotate({ identifier: 'CiTools.Deploy.WorkflowReportLink' }) -const DeployWorkflowReportRecordData = Schema.Record({ - key: Schema.NonEmptyTrimmedString, - value: Schema.Unknown, -}).annotations({ identifier: 'CiTools.Deploy.WorkflowReportRecordData' }) +const DeployWorkflowReportRecordData = Schema.Record( + Schema.Trimmed.check(Schema.isNonEmpty()), + Schema.Unknown, +).annotate({ identifier: 'CiTools.Deploy.WorkflowReportRecordData' }) const DeployWorkflowReportRecord = Schema.TaggedStruct('WorkflowReportRecord', { schemaVersion: Schema.Literal(1), - id: Schema.NonEmptyTrimmedString, - kind: Schema.NonEmptyTrimmedString, + id: Schema.Trimmed.check(Schema.isNonEmpty()), + kind: Schema.Trimmed.check(Schema.isNonEmpty()), subject: DeployWorkflowReportSubject, - status: Schema.Literal('success', 'failure', 'skipped', 'neutral'), - title: Schema.NonEmptyTrimmedString, + status: Schema.Literals(['success', 'failure', 'skipped', 'neutral']), + title: Schema.Trimmed.check(Schema.isNonEmpty()), summary: Schema.optional(Schema.String), - createdAtUtc: Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/u), + createdAtUtc: Schema.Trimmed.check(Schema.isNonEmpty()).check( + Schema.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/u)), ), links: Schema.optional(Schema.Array(DeployWorkflowReportLink)), data: Schema.optional(DeployWorkflowReportRecordData), -}).annotations({ identifier: 'CiTools.Deploy.WorkflowReportRecord' }) +}).annotate({ identifier: 'CiTools.Deploy.WorkflowReportRecord' }) const validateWorkflowReportRecord = (record: WorkflowReportRecord): WorkflowReportRecord => Schema.decodeUnknownSync(DeployWorkflowReportRecord)(record) as WorkflowReportRecord @@ -509,34 +516,34 @@ export const deploySkippedRecord = (opts: DeployRecordContext & { readonly reaso }, }) -export const DeploySpanName = Schema.Literal( +export const DeploySpanName = Schema.Literals([ 'ci-tools.deploy', 'ci-tools.deploy.provider', 'ci-tools.deploy.attempt', 'ci-tools.deploy.verify', 'ci-tools.deploy.cleanup', -).annotations({ identifier: 'CiTools.Deploy.SpanName' }) +]).annotate({ identifier: 'CiTools.Deploy.SpanName' }) export type DeploySpanName = typeof DeploySpanName.Type export const DeploySpanAttributes = Schema.Struct({ - 'span.label': Schema.NonEmptyTrimmedString.pipe( - Schema.maxLength(40), - Schema.annotations({ identifier: 'CiTools.Deploy.SpanLabel' }), + 'span.label': Schema.Trimmed.check(Schema.isNonEmpty()).pipe( + Schema.check(Schema.isMaxLength(40)), + Schema.annotate({ identifier: 'CiTools.Deploy.SpanLabel' }), ), 'ci_tools.deploy.provider': DeployProvider, 'ci_tools.deploy.target': DeployTarget, 'ci_tools.deploy.mode': Schema.optional(DeployMode), 'ci_tools.deploy.operation': Schema.optional( - Schema.Literal('core', 'provider', 'attempt', 'verify', 'cleanup'), + Schema.Literals(['core', 'provider', 'attempt', 'verify', 'cleanup']), ), 'ci_tools.deploy.attempt': Schema.optional(PositiveInt), 'ci_tools.deploy.status': Schema.optional(DeployStatus), - 'ci_tools.deploy.error_kind': Schema.optional(Schema.NonEmptyTrimmedString), + 'ci_tools.deploy.error_kind': Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), 'ci_tools.deploy.cleanup_status': Schema.optional( - Schema.Literal('succeeded', 'failed', 'skipped'), + Schema.Literals(['succeeded', 'failed', 'skipped']), ), - 'ci_tools.deploy.url_host': Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'CiTools.Deploy.SpanAttributes' }) + 'ci_tools.deploy.url_host': Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'CiTools.Deploy.SpanAttributes' }) export type DeploySpanAttributes = typeof DeploySpanAttributes.Type const shortSpanLabel = (value: string) => value.slice(0, 40) diff --git a/packages/@overeng/ci-tools/src/deploy-io.ts b/packages/@overeng/ci-tools/src/deploy-io.ts index 4d47f01fd2..81300baf2c 100644 --- a/packages/@overeng/ci-tools/src/deploy-io.ts +++ b/packages/@overeng/ci-tools/src/deploy-io.ts @@ -26,7 +26,7 @@ const appendGithubOutput = (opts: { } export const encodeWorkflowReportRecord = Effect.fn('ci-tools.deploy.io.encode-record')( - (record: WorkflowReportRecord) => Schema.encode(Schema.parseJson(Schema.Unknown))(record), + (record: WorkflowReportRecord) => Schema.encode(Schema.fromJsonString(Schema.Unknown))(record), ) export const emitWorkflowReportRecord = Effect.fn('ci-tools.deploy.io.emit-record')( diff --git a/packages/@overeng/ci-tools/src/deploy-netlify.ts b/packages/@overeng/ci-tools/src/deploy-netlify.ts index 851a011fc7..64b67d5328 100644 --- a/packages/@overeng/ci-tools/src/deploy-netlify.ts +++ b/packages/@overeng/ci-tools/src/deploy-netlify.ts @@ -3,8 +3,8 @@ import { spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' -import { HttpClient, HttpClientRequest } from '@effect/platform' import { Effect, Either, Schema } from 'effect' +import { HttpClient, HttpClientRequest } from 'effect/unstable/http' import { DeployInputV1, @@ -61,21 +61,21 @@ export type NetlifyDeployCommandOptions = { readonly e2eVerifyText?: string | undefined } -const HttpsUrlString = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^https:\/\/[^\s]+$/u), - Schema.annotations({ identifier: 'CiTools.Netlify.HttpsUrlString' }), +const HttpsUrlString = Schema.Trimmed.check(Schema.isNonEmpty()).check( + Schema.check(Schema.isPattern(/^https:\/\/[^\s]+$/u)), + Schema.annotate({ identifier: 'CiTools.Netlify.HttpsUrlString' }), ) const NetlifyDeployJson = Schema.Struct({ - deploy_id: Schema.NonEmptyTrimmedString, - site_name: Schema.NonEmptyTrimmedString, + deploy_id: Schema.Trimmed.check(Schema.isNonEmpty()), + site_name: Schema.Trimmed.check(Schema.isNonEmpty()), deploy_url: HttpsUrlString, -}).annotations({ identifier: 'CiTools.Netlify.DeployJson' }) +}).annotate({ identifier: 'CiTools.Netlify.DeployJson' }) const NetlifySiteJson = Schema.Struct({ - name: Schema.optional(Schema.NonEmptyTrimmedString), - account_slug: Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'CiTools.Netlify.SiteJson' }) + name: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), + account_slug: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'CiTools.Netlify.SiteJson' }) const isoNow = () => new Date().toISOString() @@ -251,7 +251,7 @@ const resolveNetlifySite = Effect.fn('ci-tools.deploy.netlify.resolve-site')(fun }) } - const decoded = Schema.decodeUnknownEither(Schema.parseJson(NetlifySiteJson))(response.text) + const decoded = Schema.decodeUnknownEither(Schema.fromJsonString(NetlifySiteJson))(response.text) if (Either.isLeft(decoded) === true) { return yield* new ProviderProjectLookupFailed({ provider: 'netlify', @@ -328,7 +328,7 @@ const parseDeployJson = Effect.fn('ci-tools.deploy.netlify.parse-json')(function readonly stdout: string readonly authToken: string }) { - const decoded = Schema.decodeUnknownEither(Schema.parseJson(NetlifyDeployJson))(opts.stdout) + const decoded = Schema.decodeUnknownEither(Schema.fromJsonString(NetlifyDeployJson))(opts.stdout) if (Either.isRight(decoded) === true) { return decoded.right } diff --git a/packages/@overeng/ci-tools/src/deploy-vercel.ts b/packages/@overeng/ci-tools/src/deploy-vercel.ts index 28d23e9fd5..9359837bb6 100644 --- a/packages/@overeng/ci-tools/src/deploy-vercel.ts +++ b/packages/@overeng/ci-tools/src/deploy-vercel.ts @@ -15,8 +15,8 @@ import { import { tmpdir } from 'node:os' import { join } from 'node:path' -import { HttpClient, HttpClientRequest } from '@effect/platform' import { Effect, Either, Schema } from 'effect' +import { HttpClient, HttpClientRequest } from 'effect/unstable/http' import { DeployInputV1, @@ -79,17 +79,17 @@ export type VercelDeployCommandOptions = { } const VercelProjectJson = Schema.Struct({ - id: Schema.optional(Schema.NonEmptyTrimmedString), - name: Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'CiTools.Vercel.ProjectJson' }) + id: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), + name: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'CiTools.Vercel.ProjectJson' }) -const VercelProjectFileJson = Schema.parseJson( +const VercelProjectFileJson = Schema.fromJsonString( Schema.Struct({ - projectId: Schema.NonEmptyTrimmedString, - orgId: Schema.NonEmptyTrimmedString, + projectId: Schema.Trimmed.check(Schema.isNonEmpty()), + orgId: Schema.Trimmed.check(Schema.isNonEmpty()), }), ) -const JsonUnknown = Schema.parseJson(Schema.Unknown) +const JsonUnknown = Schema.fromJsonString(Schema.Unknown) const isoNow = () => new Date().toISOString() @@ -303,7 +303,9 @@ const resolveVercelProject = Effect.fn('ci-tools.deploy.vercel.resolve-project') }) } - const decoded = Schema.decodeUnknownEither(Schema.parseJson(VercelProjectJson))(response.text) + const decoded = Schema.decodeUnknownEither(Schema.fromJsonString(VercelProjectJson))( + response.text, + ) if (Either.isLeft(decoded) === true) { return yield* new ProviderProjectLookupFailed({ provider: 'vercel', diff --git a/packages/@overeng/ci-tools/src/quarantine.ts b/packages/@overeng/ci-tools/src/quarantine.ts index e2f4b80562..175ef66066 100644 --- a/packages/@overeng/ci-tools/src/quarantine.ts +++ b/packages/@overeng/ci-tools/src/quarantine.ts @@ -27,19 +27,18 @@ export const QuarantineEntry = Schema.Struct({ issue: Schema.NonEmptyString, /** `YYYY-MM-DD`. Past this date the entry is expired and the ledger check fails. */ expires: Schema.NonEmptyString, -}).annotations({ identifier: 'CiTools.Quarantine.Entry' }) +}).annotate({ identifier: 'CiTools.Quarantine.Entry' }) export type QuarantineEntry = typeof QuarantineEntry.Type /** Every currently-tolerated test failure in a repository, keyed by quarantine key. */ -export const QuarantineLedger = Schema.Record({ - key: Schema.String, - value: QuarantineEntry, -}).annotations({ identifier: 'CiTools.Quarantine.Ledger' }) +export const QuarantineLedger = Schema.Record(Schema.String, QuarantineEntry).annotate({ + identifier: 'CiTools.Quarantine.Ledger', +}) export type QuarantineLedger = typeof QuarantineLedger.Type /** Parses ledger JSON, rejecting entries that omit a target, reason, issue, or expiry. */ export const decodeQuarantineLedgerJson = Schema.decodeUnknownSync( - Schema.parseJson(QuarantineLedger), + Schema.fromJsonString(QuarantineLedger), ) /** diff --git a/packages/@overeng/content-address/package.json b/packages/@overeng/content-address/package.json index f0b7409a87..0e7443e755 100644 --- a/packages/@overeng/content-address/package.json +++ b/packages/@overeng/content-address/package.json @@ -14,10 +14,10 @@ }, "dependencies": { "@noble/hashes": "2.2.0", - "effect": "3.21.4" + "effect": "4.0.0-beta.102" }, "devDependencies": { - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", "typescript": "6.0.3", diff --git a/packages/@overeng/content-address/src/mod.ts b/packages/@overeng/content-address/src/mod.ts index 85ac30e2f2..731c83fbca 100644 --- a/packages/@overeng/content-address/src/mod.ts +++ b/packages/@overeng/content-address/src/mod.ts @@ -7,67 +7,67 @@ import { bytesToHex } from '@noble/hashes/utils.js' import { Effect, Schema } from 'effect' /** Branded SHA-256 content digest in lowercase-hex `sha256:<64 hex>` form. */ -export const ContentDigest = Schema.String.pipe( - Schema.pattern(/^sha256:[a-f0-9]{64}$/), +export const ContentDigest = Schema.String.check(Schema.isPattern(/^sha256:[a-f0-9]{64}$/)).pipe( Schema.brand('ContentAddress.ContentDigest'), - Schema.annotations({ identifier: 'ContentAddress.ContentDigest' }), + Schema.annotate({ identifier: 'ContentAddress.ContentDigest' }), ) export type ContentDigest = typeof ContentDigest.Type /** Location-independent CAS retrieval URI in `cas:sha256//` form. */ -export const CasUri = Schema.String.pipe( - Schema.pattern(/^cas:sha256\/[a-f0-9]{2}\/[a-f0-9]{62}$/), +export const CasUri = Schema.String.check( + Schema.isPattern(/^cas:sha256\/[a-f0-9]{2}\/[a-f0-9]{62}$/), +).pipe( Schema.brand('ContentAddress.CasUri'), - Schema.annotations({ identifier: 'ContentAddress.CasUri' }), + Schema.annotate({ identifier: 'ContentAddress.CasUri' }), ) export type CasUri = typeof CasUri.Type /** Branded non-empty media (MIME) type describing the encoded byte payload. */ -export const MediaType = Schema.NonEmptyTrimmedString.pipe( +export const MediaType = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('ContentAddress.MediaType'), - Schema.annotations({ identifier: 'ContentAddress.MediaType' }), + Schema.annotate({ identifier: 'ContentAddress.MediaType' }), ) export type MediaType = typeof MediaType.Type /** Branded codec tag naming the byte-encoding scheme (e.g. `canonical-json`). */ -export const Codec = Schema.NonEmptyTrimmedString.pipe( +export const Codec = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('ContentAddress.Codec'), - Schema.annotations({ identifier: 'ContentAddress.Codec' }), + Schema.annotate({ identifier: 'ContentAddress.Codec' }), ) export type Codec = typeof Codec.Type /** Self-describing content-address record: digest + byte length + media type (optional codec/schema version). */ export const ContentDescriptor = Schema.TaggedStruct('ContentDescriptor', { digest: ContentDigest, - byteLength: Schema.NonNegativeInt, + byteLength: Schema.Natural, mediaType: MediaType, codec: Schema.optional(Codec), - schemaVersion: Schema.optional(Schema.NonNegativeInt), -}).annotations({ identifier: 'ContentAddress.ContentDescriptor' }) + schemaVersion: Schema.optional(Schema.Natural), +}).annotate({ identifier: 'ContentAddress.ContentDescriptor' }) export type ContentDescriptor = typeof ContentDescriptor.Type /** One child object referenced by a manifest, optionally with a stable logical path and role. */ export const ContentManifestEntry = Schema.Struct({ descriptor: ContentDescriptor, - logicalPath: Schema.optional(Schema.NonEmptyTrimmedString), - role: Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'ContentAddress.ContentManifestEntry' }) + logicalPath: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), + role: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'ContentAddress.ContentManifestEntry' }) export type ContentManifestEntry = typeof ContentManifestEntry.Type /** Versioned CAS manifest containing descriptors for a logical artifact or artifact set. */ export const ContentManifest = Schema.TaggedStruct('ContentManifest', { schemaVersion: Schema.Literal(1), - role: Schema.NonEmptyTrimmedString, + role: Schema.Trimmed.check(Schema.isNonEmpty()), createdAt: Schema.optional(Schema.DateTimeUtc), entries: Schema.Array(ContentManifestEntry), -}).annotations({ identifier: 'ContentAddress.ContentManifest' }) +}).annotate({ identifier: 'ContentAddress.ContentManifest' }) export type ContentManifest = typeof ContentManifest.Type /** Durable pin record that points a mutable name at an immutable manifest descriptor. */ export const ContentPin = Schema.TaggedStruct('ContentPin', { schemaVersion: Schema.Literal(1), target: ContentDescriptor, -}).annotations({ identifier: 'ContentAddress.ContentPin' }) +}).annotate({ identifier: 'ContentAddress.ContentPin' }) export type ContentPin = typeof ContentPin.Type /** Local filesystem-backed content store rooted at an absolute directory. */ @@ -77,20 +77,20 @@ export interface FileSystemContentStore { } /** Raised by {@link verifyDescriptor} when bytes fail the descriptor's digest or byte-length check (fail-closed). */ -export class ContentDescriptorMismatchError extends Schema.TaggedError()( +export class ContentDescriptorMismatchError extends Schema.TaggedErrorClass()( 'ContentDescriptorMismatchError', { expectedDigest: ContentDigest, actualDigest: ContentDigest, - expectedByteLength: Schema.NonNegativeInt, - actualByteLength: Schema.NonNegativeInt, + expectedByteLength: Schema.Natural, + actualByteLength: Schema.Natural, mediaType: MediaType, message: Schema.String, }, ) {} /** Raised when an object addressed by a descriptor is absent from the store. */ -export class ContentObjectMissingError extends Schema.TaggedError()( +export class ContentObjectMissingError extends Schema.TaggedErrorClass()( 'ContentObjectMissingError', { path: Schema.String, @@ -100,7 +100,7 @@ export class ContentObjectMissingError extends Schema.TaggedError()( +export class ContentPinMissingError extends Schema.TaggedErrorClass()( 'ContentPinMissingError', { name: Schema.String, @@ -110,18 +110,18 @@ export class ContentPinMissingError extends Schema.TaggedError()( +export class ContentStoreIoError extends Schema.TaggedErrorClass()( 'ContentStoreIoError', { operation: Schema.String, path: Schema.String, - cause: Schema.Defect, + cause: Schema.Defect(), message: Schema.String, }, ) {} /** Raised when a CAS URI and caller-provided descriptor identify different objects. */ -export class CasUriDescriptorMismatchError extends Schema.TaggedError()( +export class CasUriDescriptorMismatchError extends Schema.TaggedErrorClass()( 'CasUriDescriptorMismatchError', { uri: CasUri, @@ -132,17 +132,17 @@ export class CasUriDescriptorMismatchError extends Schema.TaggedError()( +export class InvalidCasUriError extends Schema.TaggedErrorClass()( 'InvalidCasUriError', { uri: Schema.String, - cause: Schema.Defect, + cause: Schema.Defect(), message: Schema.String, }, ) {} /** Raised when resolving a URI whose scheme is not `cas`. */ -export class UnsupportedCasSchemeError extends Schema.TaggedError()( +export class UnsupportedCasSchemeError extends Schema.TaggedErrorClass()( 'UnsupportedCasSchemeError', { uri: Schema.String, @@ -152,7 +152,7 @@ export class UnsupportedCasSchemeError extends Schema.TaggedError()( +export class UnsupportedDigestAlgorithmError extends Schema.TaggedErrorClass()( 'UnsupportedDigestAlgorithmError', { uri: Schema.String, @@ -162,7 +162,7 @@ export class UnsupportedDigestAlgorithmError extends Schema.TaggedError()( +export class UnsafePinNameError extends Schema.TaggedErrorClass()( 'UnsafePinNameError', { name: Schema.String, @@ -171,34 +171,34 @@ export class UnsafePinNameError extends Schema.TaggedError() ) {} /** Raised when a pin target is not a canonical JSON ContentManifest descriptor. */ -export class InvalidManifestDescriptorError extends Schema.TaggedError()( +export class InvalidManifestDescriptorError extends Schema.TaggedErrorClass()( 'InvalidManifestDescriptorError', { digest: ContentDigest, mediaType: MediaType, codec: Schema.optional(Codec), - schemaVersion: Schema.optional(Schema.NonNegativeInt), + schemaVersion: Schema.optional(Schema.Natural), message: Schema.String, }, ) {} /** Raised when a pin file cannot be decoded as a versioned ContentPin record. */ -export class InvalidPinRecordError extends Schema.TaggedError()( +export class InvalidPinRecordError extends Schema.TaggedErrorClass()( 'InvalidPinRecordError', { name: Schema.String, path: Schema.String, - cause: Schema.Defect, + cause: Schema.Defect(), message: Schema.String, }, ) {} /** Raised when descriptor-addressed manifest bytes cannot be decoded as a ContentManifest. */ -export class InvalidManifestRecordError extends Schema.TaggedError()( +export class InvalidManifestRecordError extends Schema.TaggedErrorClass()( 'InvalidManifestRecordError', { digest: ContentDigest, - cause: Schema.Defect, + cause: Schema.Defect(), message: Schema.String, }, ) {} @@ -273,7 +273,7 @@ const canonicalizeJson = (value: unknown): string => { } /** Encode `value` and render it as canonical JSON with object keys sorted, for stable hashing across key-insertion order. */ -export const canonicalJsonString = ({ +export const canonicalJsonString = >({ schema, value, }: { @@ -282,7 +282,7 @@ export const canonicalJsonString = ( }): string => canonicalizeJson(Schema.encodeSync(schema)(value)) /** UTF-8 bytes of {@link canonicalJsonString} — the exact bytes that get hashed. */ -export const canonicalJsonBytes = ({ +export const canonicalJsonBytes = >({ schema, value, }: { @@ -291,7 +291,7 @@ export const canonicalJsonBytes = ({ }): Uint8Array => utf8Bytes(canonicalJsonString({ schema, value })) /** Content digest of a value's canonical-JSON encoding; stable regardless of object key order. */ -export const hashCanonicalJson = ({ +export const hashCanonicalJson = >({ schema, value, }: { @@ -340,7 +340,7 @@ export const descriptorForUtf8 = ({ }) /** Build a descriptor for a value's canonical-JSON encoding; stamps the canonical-JSON codec and media type and requires an explicit `schemaVersion`. */ -export const descriptorForCanonicalJson = ({ +export const descriptorForCanonicalJson = >({ schema, value, schemaVersion, @@ -545,8 +545,8 @@ const verifyManifestDescriptor = Effect.fn('ContentAddress.verifyManifestDescrip } }) -const decodeContentPinJson = Schema.decodeUnknownSync(Schema.parseJson(ContentPin)) -const decodeContentManifestJson = Schema.decodeUnknownSync(Schema.parseJson(ContentManifest)) +const decodeContentPinJson = Schema.decodeUnknownSync(Schema.fromJsonString(ContentPin)) +const decodeContentManifestJson = Schema.decodeUnknownSync(Schema.fromJsonString(ContentManifest)) /** Store bytes under their descriptor-derived object path and return the descriptor. */ export const putBytes = Effect.fn('ContentAddress.putBytes')(function* ({ diff --git a/packages/@overeng/content-address/src/mod.unit.test.ts b/packages/@overeng/content-address/src/mod.unit.test.ts index 3ae898aa8d..2829842dfd 100644 --- a/packages/@overeng/content-address/src/mod.unit.test.ts +++ b/packages/@overeng/content-address/src/mod.unit.test.ts @@ -40,10 +40,11 @@ import { const Payload = Schema.Struct({ alpha: Schema.String, nested: Schema.Struct({ + // @effect-diagnostics-next-line schemaNumber:off -- this test preserves the existing unrestricted-number codec contract; narrowing it to finite numbers would be a behavior change. zed: Schema.Number, beta: Schema.Array(Schema.String), }), -}).annotations({ identifier: 'ContentAddressTest.Payload' }) +}).annotate({ identifier: 'ContentAddressTest.Payload' }) describe('@overeng/content-address', () => { it('hashes canonical JSON independent of object key insertion order', () => { @@ -59,7 +60,8 @@ describe('@overeng/content-address', () => { // Keys where locale order diverges from code-unit order: en-US collation sorts // 'a' before 'B', but UTF-16 code units put 'B' (66) before 'a' (97). A // locale-sensitive comparator would canonicalize this differently per machine. - const Record = Schema.Record({ key: Schema.String, value: Schema.Number }) + // @effect-diagnostics-next-line schemaNumber:off -- canonical JSON ordering is under test, and narrowing the existing arbitrary-number value schema would change the accepted input domain. + const Record = Schema.Record(Schema.String, Schema.Number) const rendered = canonicalJsonString({ schema: Record, value: { a: 1, B: 0 } }) expect(rendered).toBe('{"B":0,"a":1}') @@ -89,11 +91,11 @@ describe('@overeng/content-address', () => { Effect.runPromise(verifyDescriptor({ descriptor, bytes: utf8Bytes('hello') })), ).resolves.toBeUndefined() const mismatch = await Effect.runPromise( - verifyDescriptor({ descriptor, bytes: utf8Bytes('HELLO') }).pipe(Effect.either), + verifyDescriptor({ descriptor, bytes: utf8Bytes('HELLO') }).pipe(Effect.result), ) - expect(mismatch._tag).toBe('Left') - if (mismatch._tag === 'Left') { - expect(mismatch.left).toBeInstanceOf(ContentDescriptorMismatchError) + expect(mismatch._tag).toBe('Failure') + if (mismatch._tag === 'Failure') { + expect(mismatch.failure).toBeInstanceOf(ContentDescriptorMismatchError) } }) @@ -138,12 +140,12 @@ describe('@overeng/content-address', () => { ) const mismatch = await Effect.runPromise( - resolveCasUri({ store, uri: casUriForDescriptor(other), descriptor }).pipe(Effect.either), + resolveCasUri({ store, uri: casUriForDescriptor(other), descriptor }).pipe(Effect.result), ) - expect(mismatch._tag).toBe('Left') - if (mismatch._tag === 'Left') { - expect(mismatch.left).toBeInstanceOf(CasUriDescriptorMismatchError) + expect(mismatch._tag).toBe('Failure') + if (mismatch._tag === 'Failure') { + expect(mismatch.failure).toBeInstanceOf(CasUriDescriptorMismatchError) } } finally { await rm(root, { recursive: true, force: true }) @@ -159,12 +161,12 @@ describe('@overeng/content-address', () => { ) const invalid = await Effect.runPromise( - resolveCasUri({ store, uri: 'cas:sha256/aa/not-enough', descriptor }).pipe(Effect.either), + resolveCasUri({ store, uri: 'cas:sha256/aa/not-enough', descriptor }).pipe(Effect.result), ) - expect(invalid._tag).toBe('Left') - if (invalid._tag === 'Left') { - expect(invalid.left).toBeInstanceOf(InvalidCasUriError) + expect(invalid._tag).toBe('Failure') + if (invalid._tag === 'Failure') { + expect(invalid.failure).toBeInstanceOf(InvalidCasUriError) } } finally { await rm(root, { recursive: true, force: true }) @@ -180,21 +182,21 @@ describe('@overeng/content-address', () => { ) const unsupportedScheme = await Effect.runPromise( - resolveCasUri({ store, uri: 'file:///tmp/profile', descriptor }).pipe(Effect.either), + resolveCasUri({ store, uri: 'file:///tmp/profile', descriptor }).pipe(Effect.result), ) const unsupportedAlgorithm = await Effect.runPromise( resolveCasUri({ store, uri: `cas:blake3/${'a'.repeat(64)}`, descriptor }).pipe( - Effect.either, + Effect.result, ), ) - expect(unsupportedScheme._tag).toBe('Left') - if (unsupportedScheme._tag === 'Left') { - expect(unsupportedScheme.left).toBeInstanceOf(UnsupportedCasSchemeError) + expect(unsupportedScheme._tag).toBe('Failure') + if (unsupportedScheme._tag === 'Failure') { + expect(unsupportedScheme.failure).toBeInstanceOf(UnsupportedCasSchemeError) } - expect(unsupportedAlgorithm._tag).toBe('Left') - if (unsupportedAlgorithm._tag === 'Left') { - expect(unsupportedAlgorithm.left).toBeInstanceOf(UnsupportedDigestAlgorithmError) + expect(unsupportedAlgorithm._tag).toBe('Failure') + if (unsupportedAlgorithm._tag === 'Failure') { + expect(unsupportedAlgorithm.failure).toBeInstanceOf(UnsupportedDigestAlgorithmError) } } finally { await rm(root, { recursive: true, force: true }) @@ -210,12 +212,12 @@ describe('@overeng/content-address', () => { ) const invalid = await Effect.runPromise( - resolveCasUri({ store, uri: 'cas:sha256/not-enough', descriptor }).pipe(Effect.either), + resolveCasUri({ store, uri: 'cas:sha256/not-enough', descriptor }).pipe(Effect.result), ) - expect(invalid._tag).toBe('Left') - if (invalid._tag === 'Left') { - expect(invalid.left).toBeInstanceOf(InvalidCasUriError) + expect(invalid._tag).toBe('Failure') + if (invalid._tag === 'Failure') { + expect(invalid.failure).toBeInstanceOf(InvalidCasUriError) } } finally { await rm(root, { recursive: true, force: true }) @@ -259,26 +261,26 @@ describe('@overeng/content-address', () => { putBytes({ store, bytes: utf8Bytes('profile'), mediaType: 'application/octet-stream' }), ) const unsafe = await Effect.runPromise( - pinManifest({ store, name: '../escape', manifestDescriptor: artifact }).pipe(Effect.either), + pinManifest({ store, name: '../escape', manifestDescriptor: artifact }).pipe(Effect.result), ) const dot = await Effect.runPromise( - pinManifest({ store, name: '.', manifestDescriptor: artifact }).pipe(Effect.either), + pinManifest({ store, name: '.', manifestDescriptor: artifact }).pipe(Effect.result), ) const dotted = await Effect.runPromise( - pinManifest({ store, name: 'runs/.', manifestDescriptor: artifact }).pipe(Effect.either), + pinManifest({ store, name: 'runs/.', manifestDescriptor: artifact }).pipe(Effect.result), ) - expect(unsafe._tag).toBe('Left') - if (unsafe._tag === 'Left') { - expect(unsafe.left).toBeInstanceOf(UnsafePinNameError) + expect(unsafe._tag).toBe('Failure') + if (unsafe._tag === 'Failure') { + expect(unsafe.failure).toBeInstanceOf(UnsafePinNameError) } - expect(dot._tag).toBe('Left') - if (dot._tag === 'Left') { - expect(dot.left).toBeInstanceOf(UnsafePinNameError) + expect(dot._tag).toBe('Failure') + if (dot._tag === 'Failure') { + expect(dot.failure).toBeInstanceOf(UnsafePinNameError) } - expect(dotted._tag).toBe('Left') - if (dotted._tag === 'Left') { - expect(dotted.left).toBeInstanceOf(UnsafePinNameError) + expect(dotted._tag).toBe('Failure') + if (dotted._tag === 'Failure') { + expect(dotted.failure).toBeInstanceOf(UnsafePinNameError) } } finally { await rm(root, { recursive: true, force: true }) @@ -314,13 +316,13 @@ describe('@overeng/content-address', () => { ) const invalidPin = await Effect.runPromise( pinManifest({ store, name: 'runs/run-1', manifestDescriptor: artifact }).pipe( - Effect.either, + Effect.result, ), ) - expect(invalidPin._tag).toBe('Left') - if (invalidPin._tag === 'Left') { - expect(invalidPin.left).toBeInstanceOf(InvalidManifestDescriptorError) + expect(invalidPin._tag).toBe('Failure') + if (invalidPin._tag === 'Failure') { + expect(invalidPin.failure).toBeInstanceOf(InvalidManifestDescriptorError) } } finally { await rm(root, { recursive: true, force: true }) @@ -344,13 +346,13 @@ describe('@overeng/content-address', () => { const invalidPin = await Effect.runPromise( pinManifest({ store, name: 'runs/run-1', manifestDescriptor: missingDescriptor }).pipe( - Effect.either, + Effect.result, ), ) - expect(invalidPin._tag).toBe('Left') - if (invalidPin._tag === 'Left') { - expect(invalidPin.left).toBeInstanceOf(ContentObjectMissingError) + expect(invalidPin._tag).toBe('Failure') + if (invalidPin._tag === 'Failure') { + expect(invalidPin.failure).toBeInstanceOf(ContentObjectMissingError) } } finally { await rm(root, { recursive: true, force: true }) @@ -376,12 +378,12 @@ describe('@overeng/content-address', () => { store, name: 'runs/run-1', manifestDescriptor: malformedManifestDescriptor, - }).pipe(Effect.either), + }).pipe(Effect.result), ) - expect(invalidPin._tag).toBe('Left') - if (invalidPin._tag === 'Left') { - expect(invalidPin.left).toBeInstanceOf(InvalidManifestRecordError) + expect(invalidPin._tag).toBe('Failure') + if (invalidPin._tag === 'Failure') { + expect(invalidPin.failure).toBeInstanceOf(InvalidManifestRecordError) } } finally { await rm(root, { recursive: true, force: true }) @@ -414,12 +416,12 @@ describe('@overeng/content-address', () => { store, name: 'runs/run-1', manifestDescriptor: nonCanonicalManifestDescriptor, - }).pipe(Effect.either), + }).pipe(Effect.result), ) - expect(invalidPin._tag).toBe('Left') - if (invalidPin._tag === 'Left') { - expect(invalidPin.left).toBeInstanceOf(InvalidManifestRecordError) + expect(invalidPin._tag).toBe('Failure') + if (invalidPin._tag === 'Failure') { + expect(invalidPin.failure).toBeInstanceOf(InvalidManifestRecordError) } } finally { await rm(root, { recursive: true, force: true }) diff --git a/packages/@overeng/effect-ai-claude-cli/package.json b/packages/@overeng/effect-ai-claude-cli/package.json index 2ddf6b2eb4..4101013727 100644 --- a/packages/@overeng/effect-ai-claude-cli/package.json +++ b/packages/@overeng/effect-ai-claude-cli/package.json @@ -13,19 +13,15 @@ } }, "devDependencies": { - "@effect/ai": "0.36.0", - "@effect/platform": "0.96.2", - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vite": "8.0.16", "vitest": "4.1.9" }, "peerDependencies": { - "@effect/ai": "^0.36.0", - "@effect/platform": "^0.96.2", - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/effect-ai-claude-cli/package.json.genie.ts b/packages/@overeng/effect-ai-claude-cli/package.json.genie.ts index bbf7b02fe1..f05518134c 100644 --- a/packages/@overeng/effect-ai-claude-cli/package.json.genie.ts +++ b/packages/@overeng/effect-ai-claude-cli/package.json.genie.ts @@ -9,21 +9,13 @@ import { } from '../../../genie/internal.ts' import utilsDevPkg from '../utils-dev/package.json.genie.ts' -const peerDepNames = ['@effect/ai', '@effect/platform', 'effect'] as const +const peerDepNames = ['effect'] as const const workspaceDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/effect-ai-claude-cli' }), devDependencies: { workspace: [utilsDevPkg], external: { - ...catalog.pick( - '@effect/ai', - '@effect/platform', - '@effect/vitest', - 'typescript', - 'effect', - 'vite', - 'vitest', - ), + ...catalog.pick('@effect/vitest', 'typescript', 'effect', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/effect-ai-claude-cli/src/claude-cli.ts b/packages/@overeng/effect-ai-claude-cli/src/claude-cli.ts index 8739b2b1b9..f987d6b83d 100644 --- a/packages/@overeng/effect-ai-claude-cli/src/claude-cli.ts +++ b/packages/@overeng/effect-ai-claude-cli/src/claude-cli.ts @@ -4,10 +4,10 @@ * Implements the LanguageModel interface by delegating to the `claude` CLI, * allowing use without API keys by re-using Claude CLI authentication. */ -import { AiError, LanguageModel, type Prompt, type Response } from '@effect/ai' -import { Command, CommandExecutor } from '@effect/platform' -import type { PlatformError } from '@effect/platform/Error' -import { Effect, Exit, flow, JSONSchema, Layer, Ref, Scope, Stream } from 'effect' +import { AiError, LanguageModel, type Prompt, type Response } from 'effect/unstable/ai' +import { ChildProcess as Command, ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' +import type { PlatformError } from 'effect/Error' +import { Effect, Exit, flow, JsonSchema, Layer, Ref, Scope, Stream } from 'effect' import { ClaudeCliAuthError, @@ -78,7 +78,7 @@ const promptToString = (opts: { // Add JSON schema instructions if JSON response format is requested if (opts.responseFormat.type === 'json') { - const jsonSchema = JSONSchema.make(opts.responseFormat.schema) + const jsonSchema = JsonSchema.make(opts.responseFormat.schema) parts.push( `[System]: CRITICAL: Your response must be ONLY raw JSON. Do NOT use markdown code blocks (\`\`\`). Do NOT add any explanation before or after. Start your response with { and end with }. The JSON must conform to this schema:\n${JSON.stringify(jsonSchema, null, 2)}`, ) diff --git a/packages/@overeng/effect-ai-claude-cli/src/errors.ts b/packages/@overeng/effect-ai-claude-cli/src/errors.ts index 8b5737a13b..8838b2a9d9 100644 --- a/packages/@overeng/effect-ai-claude-cli/src/errors.ts +++ b/packages/@overeng/effect-ai-claude-cli/src/errors.ts @@ -7,16 +7,16 @@ import { Schema } from 'effect' /** Claude CLI binary not found or not executable */ -export class ClaudeCliNotFoundError extends Schema.TaggedError()( +export class ClaudeCliNotFoundError extends Schema.TaggedErrorClass()( 'ClaudeCliNotFoundError', { message: Schema.String, - cause: Schema.Defect, + cause: Schema.Defect(), }, ) {} /** User not logged in to Claude CLI */ -export class ClaudeCliNotLoggedInError extends Schema.TaggedError()( +export class ClaudeCliNotLoggedInError extends Schema.TaggedErrorClass()( 'ClaudeCliNotLoggedInError', { message: Schema.String, @@ -24,16 +24,16 @@ export class ClaudeCliNotLoggedInError extends Schema.TaggedError()( +export class ClaudeCliAuthError extends Schema.TaggedErrorClass()( 'ClaudeCliAuthError', { message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** CLI exited with non-zero code (fallback for unrecognized errors) */ -export class ClaudeCliExitError extends Schema.TaggedError()( +export class ClaudeCliExitError extends Schema.TaggedErrorClass()( 'ClaudeCliExitError', { message: Schema.String, @@ -45,17 +45,17 @@ export class ClaudeCliExitError extends Schema.TaggedError() ) {} /** Failed to parse JSON response from CLI */ -export class ClaudeCliParseError extends Schema.TaggedError()( +export class ClaudeCliParseError extends Schema.TaggedErrorClass()( 'ClaudeCliParseError', { message: Schema.String, rawOutput: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** Rate limited by Claude API */ -export class ClaudeCliRateLimitError extends Schema.TaggedError()( +export class ClaudeCliRateLimitError extends Schema.TaggedErrorClass()( 'ClaudeCliRateLimitError', { message: Schema.String, diff --git a/packages/@overeng/effect-distributed-lock/package.json b/packages/@overeng/effect-distributed-lock/package.json index dd7383cd50..36d1c7f730 100644 --- a/packages/@overeng/effect-distributed-lock/package.json +++ b/packages/@overeng/effect-distributed-lock/package.json @@ -13,15 +13,15 @@ } }, "devDependencies": { - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/effect-distributed-lock/package.json.genie.ts b/packages/@overeng/effect-distributed-lock/package.json.genie.ts index 1c97d6a343..9f29b78cbf 100644 --- a/packages/@overeng/effect-distributed-lock/package.json.genie.ts +++ b/packages/@overeng/effect-distributed-lock/package.json.genie.ts @@ -15,13 +15,7 @@ const workspaceDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/effect-distributed-lock' }), devDependencies: { workspace: [utilsDevPkg], - external: catalog.pick( - ...peerDepNames, - '@effect/vitest', - '@types/node', - 'typescript', - 'vitest', - ), + external: catalog.pick(...peerDepNames, '@effect/vitest', '@types/node', 'typescript', 'vitest'), }, peerDependencies: { external: catalog.pick(...peerDepNames), diff --git a/packages/@overeng/effect-distributed-lock/src/Backing.ts b/packages/@overeng/effect-distributed-lock/src/Backing.ts index 6831d41384..d44c5edd47 100644 --- a/packages/@overeng/effect-distributed-lock/src/Backing.ts +++ b/packages/@overeng/effect-distributed-lock/src/Backing.ts @@ -45,6 +45,6 @@ export interface DistributedSemaphoreBacking { } /** Effect service tag for the distributed semaphore backing store. */ -export const DistributedSemaphoreBacking = Context.GenericTag( +export const DistributedSemaphoreBacking = Context.Service( '@effect-distributed-lock/DistributedSemaphoreBacking', ) diff --git a/packages/@overeng/effect-distributed-lock/src/DistributedSemaphore.ts b/packages/@overeng/effect-distributed-lock/src/DistributedSemaphore.ts index 89486ad783..8ebf308a23 100644 --- a/packages/@overeng/effect-distributed-lock/src/DistributedSemaphore.ts +++ b/packages/@overeng/effect-distributed-lock/src/DistributedSemaphore.ts @@ -1,7 +1,7 @@ /** Vendored from effect-distributed-lock 0.0.11 (MIT, Copyright (c) 2025 Ethan Niser). See NOTICE. */ /* oxlint-disable overeng/named-args -- Preserve the upstream positional semaphore API. */ import type { Scope } from 'effect' -import { Duration, Effect, Fiber, Function, Option, Schedule, Stream } from 'effect' +import { Duration, Effect, Fiber, Option, Result, Schedule, Semaphore, Stream } from 'effect' import type { SemaphoreBackingError } from './Backing.ts' import { DistributedSemaphoreBacking } from './Backing.ts' @@ -10,8 +10,8 @@ import { LockLostError, LockNotAcquiredError } from './Errors.ts' /** Runtime tuning for distributed semaphore acquisition and refresh. */ export interface DistributedSemaphoreConfig { readonly limit?: number - readonly ttl?: Duration.DurationInput - readonly refreshInterval?: Duration.DurationInput + readonly ttl?: Duration.Input + readonly refreshInterval?: Duration.Input readonly acquireRetryPolicy?: Schedule.Schedule readonly backingFailureRetryPolicy?: Schedule.Schedule } @@ -72,10 +72,10 @@ type FullyResolvedConfig = { const fullyResolveConfig = (config: DistributedSemaphoreConfig): FullyResolvedConfig => { const limit = config.limit ?? DEFAULT_LIMIT - const ttl = config.ttl !== undefined ? Duration.decode(config.ttl) : DEFAULT_TTL + const ttl = config.ttl !== undefined ? Duration.fromInputUnsafe(config.ttl) : DEFAULT_TTL const refreshInterval = config.refreshInterval !== undefined - ? Duration.decode(config.refreshInterval) + ? Duration.fromInputUnsafe(config.refreshInterval) : Duration.millis(Duration.toMillis(ttl) / 3) const acquireRetryPolicy = config.acquireRetryPolicy ?? DEFAULT_ACQUIRE_RETRY_POLICY const backingFailureRetryPolicy = config.backingFailureRetryPolicy ?? DEFAULT_FAILURE_RETRY_POLICY @@ -127,7 +127,7 @@ export const make = ( Schedule.spaced(refreshInterval), ).pipe( Effect.andThen( - Effect.dieMessage('Invariant violated: `keepAlive` should never return a value'), + Effect.die(new Error('Invariant violated: `keepAlive` should never return a value')), ), ) @@ -179,18 +179,18 @@ export const make = ( ? { identifier } : { identifier, acquiredExternally: options.acquiredExternally } - const acquireSemaphore = yield* Effect.makeSemaphore(1) + const acquireSemaphore = yield* Semaphore.make(1) const pushBasedAcquireEnabled = backing.onPermitsReleased !== undefined const pollBasedAcquire = Effect.gen(function* () { - const maybeAcquired = yield* tryTake(permits, resolvedOptions).pipe( + const acquire = tryTake(permits, resolvedOptions) + const maybeAcquired = pushBasedAcquireEnabled === true - ? Function.compose( + ? yield* acquire.pipe( acquireSemaphore.withPermitsIfAvailable(1), Effect.map(Option.flatten), ) - : Function.identity, - ) + : yield* acquire if (Option.isNone(maybeAcquired) === true) { return yield* new LockNotAcquiredError({ key }) } @@ -209,25 +209,25 @@ export const make = ( const pushBasedAcquire = Effect.gen(function* () { if (backing.onPermitsReleased === undefined) { - return yield* Effect.dieMessage( - 'Invariant violated: `onPermitsReleased` is not provided when it was expected', + return yield* Effect.die( + new Error( + 'Invariant violated: `onPermitsReleased` is not provided when it was expected', + ), ) } return yield* backing.onPermitsReleased(key).pipe( - Stream.runFoldWhileEffect( - Option.none>(), - Option.isNone, - () => - tryTake(permits, resolvedOptions).pipe( - acquireSemaphore.withPermitsIfAvailable(1), - Effect.map(Option.flatten), - ), + Stream.filterMapEffect(() => + tryTake(permits, resolvedOptions).pipe( + acquireSemaphore.withPermitsIfAvailable(1), + Effect.map(Option.flatten), + Effect.map(Result.fromOption(() => undefined)), + ), ), + Stream.runHead, Effect.flatMap( Option.match({ - onSome: Effect.succeed, - onNone: () => - Effect.dieMessage('Invariant violated: the stream should never return `None`'), + onSome: (fiber) => Effect.succeed(fiber), + onNone: () => Effect.never, }), ), ) diff --git a/packages/@overeng/effect-path/package.json b/packages/@overeng/effect-path/package.json index 293c6783df..167dc80c43 100644 --- a/packages/@overeng/effect-path/package.json +++ b/packages/@overeng/effect-path/package.json @@ -13,18 +13,16 @@ } }, "devDependencies": { - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/vitest": "0.29.0", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "@effect/platform": "^0.96.2", - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/effect-path/package.json.genie.ts b/packages/@overeng/effect-path/package.json.genie.ts index d8eb9934ca..479bad58a5 100644 --- a/packages/@overeng/effect-path/package.json.genie.ts +++ b/packages/@overeng/effect-path/package.json.genie.ts @@ -9,21 +9,14 @@ import { } from '../../../genie/internal.ts' import utilsDevPkg from '../utils-dev/package.json.genie.ts' -const peerDepNames = ['@effect/platform', 'effect'] as const +const peerDepNames = ['effect'] as const const workspaceDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/effect-path' }), devDependencies: { workspace: [utilsDevPkg], external: { - ...catalog.pick( - ...peerDepNames, - '@effect/platform-node', - '@effect/vitest', - '@types/node', - 'typescript', - 'vitest', - ), + ...catalog.pick(...peerDepNames, '@effect/platform-node', '@effect/vitest', '@types/node', 'typescript', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/effect-path/src/Sandbox.ts b/packages/@overeng/effect-path/src/Sandbox.ts index f2ca60265b..cad04886bc 100644 --- a/packages/@overeng/effect-path/src/Sandbox.ts +++ b/packages/@overeng/effect-path/src/Sandbox.ts @@ -7,8 +7,7 @@ * All operations within a sandbox are guaranteed to stay within the root directory. */ -import { FileSystem, type Path as PlatformPath } from '@effect/platform' -import { Effect, Either } from 'effect' +import { Effect, FileSystem, type Path as PlatformPath, Result } from 'effect' import type { AbsoluteDirPath, AbsolutePath, RelativePath } from './brands.ts' import { PathNotFoundError, PermissionError, TraversalError, type SandboxError } from './errors.ts' @@ -40,7 +39,7 @@ export interface Sandbox { * * @returns The normalized relative path, or TraversalError if it escapes */ - validate(path: RelativePath): Either.Either + validate(path: RelativePath): Result.Result /** * Resolve a relative path to an absolute path within the sandbox. @@ -48,7 +47,7 @@ export interface Sandbox { * * @returns The absolute path, or TraversalError if it escapes */ - resolve(path: RelativePath): Either.Either + resolve(path: RelativePath): Result.Result /** * Check if an absolute path is contained within this sandbox. @@ -111,13 +110,13 @@ export const sandbox = (root: AbsoluteDirPath): Sandbox => { * Validate that a path doesn't escape the sandbox. * Returns the normalized relative path. */ - const validate = (path: RelativePath): Either.Either => { + const validate = (path: RelativePath): Result.Result => { // Normalize the path first const normalized = lexicalPure(path) // Reject absolute paths (POSIX semantics) if (normalized.startsWith('/') === true) { - return Either.left( + return Result.fail( new TraversalError({ path, message: `Expected relative path (must not start with '/'): ${path}`, @@ -147,7 +146,7 @@ export const sandbox = (root: AbsoluteDirPath): Sandbox => { } if (escapingSegments.length > 0) { - return Either.left( + return Result.fail( new TraversalError({ path, message: `Path escapes sandbox root: ${path}`, @@ -160,24 +159,24 @@ export const sandbox = (root: AbsoluteDirPath): Sandbox => { // Preserve trailing slash if (hasTrailingSlash(path) === true) { - return Either.right(ensureTrailingSlash(normalized) as RelativePath) + return Result.succeed(ensureTrailingSlash(normalized) as RelativePath) } - return Either.right(normalized as RelativePath) + return Result.succeed(normalized as RelativePath) } /** * Resolve a relative path to absolute within the sandbox. */ - const resolve = (path: RelativePath): Either.Either => { + const resolve = (path: RelativePath): Result.Result => { const validatedResult = validate(path) - if (Either.isLeft(validatedResult) === true) { - return Either.left(validatedResult.left) + if (Result.isFailure(validatedResult) === true) { + return Result.fail(validatedResult.failure) } - const validated = validatedResult.right + const validated = validatedResult.success const relativePart = removeTrailingSlash(validated) if (relativePart === '.') { - return Either.right(ensureTrailingSlash(normalizedRoot) as AbsolutePath) + return Result.succeed(ensureTrailingSlash(normalizedRoot) as AbsolutePath) } const normalizedRelative = relativePart.replace(/^\/+/, '') @@ -185,9 +184,9 @@ export const sandbox = (root: AbsoluteDirPath): Sandbox => { normalizedRoot === '/' ? `/${normalizedRelative}` : `${normalizedRoot}/${normalizedRelative}` if (hasTrailingSlash(validated) === true) { - return Either.right(ensureTrailingSlash(absolutePath) as AbsolutePath) + return Result.succeed(ensureTrailingSlash(absolutePath) as AbsolutePath) } - return Either.right(absolutePath as AbsolutePath) + return Result.succeed(absolutePath as AbsolutePath) } /** @@ -246,32 +245,30 @@ export const sandbox = (root: AbsoluteDirPath): Sandbox => { const getSafeRealPath = Effect.fnUntraced(function* (path: RelativePath) { // First do lexical validation const resolveResult = resolve(path) - if (Either.isLeft(resolveResult) === true) { - return yield* resolveResult.left + if (Result.isFailure(resolveResult) === true) { + return yield* resolveResult.failure } - const resolved = resolveResult.right + const resolved = resolveResult.success const fs = yield* FileSystem.FileSystem // Get real path (follows symlinks) const realPath = yield* fs.realPath(resolved).pipe( Effect.mapError((error) => { - if (error._tag === 'SystemError') { - if (error.reason === 'NotFound') { - return new PathNotFoundError({ - path: resolved, - message: `Path not found: ${resolved}`, - nearestExisting: undefined, - expectedType: 'any', - }) - } - if (error.reason === 'PermissionDenied') { - return new PermissionError({ - path: resolved, - message: `Permission denied: ${resolved}`, - operation: 'stat', - }) - } + if (error.reason._tag === 'NotFound') { + return new PathNotFoundError({ + path: resolved, + message: `Path not found: ${resolved}`, + nearestExisting: undefined, + expectedType: 'any', + }) + } + if (error.reason._tag === 'PermissionDenied') { + return new PermissionError({ + path: resolved, + message: `Permission denied: ${resolved}`, + operation: 'stat', + }) } return new PathNotFoundError({ path: resolved, @@ -324,12 +321,12 @@ export const sandbox = (root: AbsoluteDirPath): Sandbox => { exists: Effect.fnUntraced(function* (path) { const resolveResult = resolve(path) - if (Either.isLeft(resolveResult) === true) { - return yield* resolveResult.left + if (Result.isFailure(resolveResult) === true) { + return yield* resolveResult.failure } - const resolved = resolveResult.right + const resolved = resolveResult.success const fs = yield* FileSystem.FileSystem - const exists = yield* fs.exists(resolved).pipe(Effect.orElse(() => Effect.succeed(false))) + const exists = yield* fs.exists(resolved).pipe(Effect.catch(() => Effect.succeed(false))) if (exists === false) { return false } @@ -338,7 +335,7 @@ export const sandbox = (root: AbsoluteDirPath): Sandbox => { Effect.flatMap((realPath) => validateRealPath({ originalPath: path, realPath }).pipe(Effect.as(true)), ), - Effect.orElse(() => Effect.succeed(false)), + Effect.catch(() => Effect.succeed(false)), ) }), @@ -362,7 +359,7 @@ export const sandbox = (root: AbsoluteDirPath): Sandbox => { const fs = yield* FileSystem.FileSystem return yield* fs.stat(safePath).pipe( Effect.mapError((error) => { - if (error._tag === 'SystemError' && error.reason === 'NotFound') { + if (error.reason._tag === 'NotFound') { return new PathNotFoundError({ path: safePath, message: `Path not found: ${safePath}`, @@ -400,7 +397,7 @@ export const withSandbox = (args: { export const validatePath = (args: { readonly root: AbsoluteDirPath readonly path: RelativePath -}): Either.Either => sandbox(args.root).validate(args.path) +}): Result.Result => sandbox(args.root).validate(args.path) /** * Check if a path would stay within a directory. diff --git a/packages/@overeng/effect-path/src/baseline.unit.test.ts b/packages/@overeng/effect-path/src/baseline.unit.test.ts index ea1c85d906..3cacff94c2 100644 --- a/packages/@overeng/effect-path/src/baseline.unit.test.ts +++ b/packages/@overeng/effect-path/src/baseline.unit.test.ts @@ -1,6 +1,5 @@ -import { FileSystem, type Error as PlatformError } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' -import { Effect, Either, Schema } from 'effect' +import { NodeServices } from '@effect/platform-node' +import { Effect, FileSystem, type PlatformError, Result, Schema } from 'effect' import { describe, expect, it } from 'vitest' import { @@ -83,18 +82,18 @@ const summarizeTraversalError = (error: TraversalError) => ({ sandboxRoot: error.sandboxRoot, }) -const left = (either: Either.Either, message = 'expected failure'): E => { - if (Either.isRight(either) === true) { +const left = (either: Result.Result, message = 'expected failure'): E => { + if (Result.isSuccess(either) === true) { throw new Error(message) } - return either.left + return either.failure } const summarizePlatformError = (error: PlatformError.PlatformError) => ({ _tag: error._tag, - reason: error._tag === 'SystemError' ? error.reason : undefined, - module: error.module, - method: error.method, + reason: error.reason._tag, + module: error.reason.module, + method: error.reason.method, }) type VerifiedFileBaselineError = @@ -129,12 +128,14 @@ describe('effect-path baselines (cross-major invariant)', () => { Effect.gen(function* () { const normalizedSchema = EffectPath.schema.AbsoluteFileInfo() const originalSchema = EffectPath.schema.AbsoluteFileInfo({ encodeAs: 'original' }) - const decoded = yield* Schema.decodeUnknown(normalizedSchema)('/repo//pkg/archive.tar.gz') + const decoded = yield* Schema.decodeUnknownEffect(normalizedSchema)( + '/repo//pkg/archive.tar.gz', + ) return { decoded: summarizeFileInfo(decoded), - encodedDefault: yield* Schema.encode(normalizedSchema)(decoded), - encodedOriginal: yield* Schema.encode(originalSchema)(decoded), + encodedDefault: yield* Schema.encodeEffect(normalizedSchema)(decoded), + encodedOriginal: yield* Schema.encodeEffect(originalSchema)(decoded), } }), ) @@ -172,16 +173,16 @@ describe('effect-path baselines (cross-major invariant)', () => { Effect.gen(function* () { const absoluteFileFromRelative = yield* EffectPath.convention .absoluteFile('relative/file.txt') - .pipe(Effect.either) + .pipe(Effect.result) const relativeFileFromAbsolute = yield* EffectPath.convention .relativeFile('/absolute/file.txt') - .pipe(Effect.either) + .pipe(Effect.result) const fileFromDirectoryConvention = yield* EffectPath.convention .absoluteFile('/absolute/dir/') - .pipe(Effect.either) + .pipe(Effect.result) const invalidPath = yield* EffectPath.convention .relativeFile('bad\0path.txt') - .pipe(Effect.either) + .pipe(Effect.result) return [ left(absoluteFileFromRelative), @@ -189,7 +190,7 @@ describe('effect-path baselines (cross-major invariant)', () => { left(fileFromDirectoryConvention), left(invalidPath), ].map(summarizeConventionError) - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) expect(result).toMatchInlineSnapshot(` @@ -235,9 +236,11 @@ describe('effect-path baselines (cross-major invariant)', () => { expect({ joined, normalized, - allowed: Either.isRight(allowed) === true ? allowed.right : allowed.left, + allowed: Result.isSuccess(allowed) === true ? allowed.success : allowed.failure, escaped: - Either.isLeft(escaped) === true ? summarizeTraversalError(escaped.left) : escaped.right, + Result.isFailure(escaped) === true + ? summarizeTraversalError(escaped.failure) + : escaped.success, }).toMatchInlineSnapshot(` { "allowed": "/repo/root/index.ts", @@ -264,13 +267,13 @@ describe('effect-path baselines (cross-major invariant)', () => { const root = yield* fs.makeTempDirectoryScoped() const missing = `${root}/missing.txt` - const enoent = left(yield* fs.stat(missing).pipe(Effect.either)) + const enoent = left(yield* fs.stat(missing).pipe(Effect.result)) const missingPath = left( - yield* EffectPath.verified.absoluteFile(missing).pipe(Effect.either), + yield* EffectPath.verified.absoluteFile(missing).pipe(Effect.result), ) // effect-path has no public create/write API or EEXIST branch. This real failure pins the // platform wrapper fields only; it does not claim a package-owned EEXIST partition. - const eexist = left(yield* fs.makeDirectory(root).pipe(Effect.either)) + const eexist = left(yield* fs.makeDirectory(root).pipe(Effect.result)) return { enoent: { @@ -281,17 +284,14 @@ describe('effect-path baselines (cross-major invariant)', () => { platform: summarizePlatformError(eexist), }, } - }).pipe(Effect.scoped, Effect.provide(NodeContext.layer)), + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ) - // TODO(live-migration:effect-3-4): v4 replaces SystemError with PlatformError (register entry - // platform-error-wrapper). Resolve by updating the tag while keeping this partition and these - // inner fields identical — a change in WHICH branch fires is a real regression, not a rename. expect(result).toMatchInlineSnapshot(` { "eexist": { "platform": { - "_tag": "SystemError", + "_tag": "PlatformError", "method": "makeDirectory", "module": "FileSystem", "reason": "AlreadyExists", @@ -299,7 +299,7 @@ describe('effect-path baselines (cross-major invariant)', () => { }, "enoent": { "platform": { - "_tag": "SystemError", + "_tag": "PlatformError", "method": "stat", "module": "FileSystem", "reason": "NotFound", @@ -329,9 +329,9 @@ describe('effect-path baselines (cross-major invariant)', () => { yield* fs.chmod(blocked, 0o000) return yield* Effect.gen(function* () { - const eacces = left(yield* fs.stat(blockedFile).pipe(Effect.either)) + const eacces = left(yield* fs.stat(blockedFile).pipe(Effect.result)) const permission = left( - yield* EffectPath.verified.absoluteFile(blockedFile).pipe(Effect.either), + yield* EffectPath.verified.absoluteFile(blockedFile).pipe(Effect.result), ) return { @@ -339,16 +339,13 @@ describe('effect-path baselines (cross-major invariant)', () => { public: summarizePathError(permission, root), } }).pipe(Effect.ensuring(fs.chmod(blocked, 0o700).pipe(Effect.ignore))) - }).pipe(Effect.scoped, Effect.provide(NodeContext.layer)), + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ) - // TODO(live-migration:effect-3-4): v4 replaces SystemError with PlatformError (register entry - // platform-error-wrapper). Resolve by updating the tag while keeping this partition and these - // inner fields identical — a change in WHICH branch fires is a real regression, not a rename. expect(result).toMatchInlineSnapshot(` { "platform": { - "_tag": "SystemError", + "_tag": "PlatformError", "method": "stat", "module": "FileSystem", "reason": "PermissionDenied", diff --git a/packages/@overeng/effect-path/src/convention.ts b/packages/@overeng/effect-path/src/convention.ts index bb72ca89b8..e413fa4bf4 100644 --- a/packages/@overeng/effect-path/src/convention.ts +++ b/packages/@overeng/effect-path/src/convention.ts @@ -8,8 +8,7 @@ * This module provides parsing functions that use the @effect/platform Path service. */ -import { Path as PlatformPath } from '@effect/platform' -import { Effect } from 'effect' +import { Effect, Path as PlatformPath } from 'effect' import type { Abs, diff --git a/packages/@overeng/effect-path/src/errors.ts b/packages/@overeng/effect-path/src/errors.ts index 71d9fbf86b..af0f5c856b 100644 --- a/packages/@overeng/effect-path/src/errors.ts +++ b/packages/@overeng/effect-path/src/errors.ts @@ -2,7 +2,7 @@ * Error types for path operations with rich metadata. * * Each error captures contextual information useful for debugging and error handling. - * All errors extend Schema.TaggedError for Effect integration. + * All errors extend Schema.TaggedErrorClass for Effect integration. */ import { Schema } from 'effect' @@ -12,57 +12,66 @@ import { Schema } from 'effect' // ═══════════════════════════════════════════════════════════════════════════ /** Reason why a path string is invalid */ -export const InvalidPathReason = Schema.Literal( +export const InvalidPathReason = Schema.Literals([ 'empty', 'null_byte', 'invalid_characters', 'reserved_name', 'too_long', -) +]) export type InvalidPathReason = typeof InvalidPathReason.Type /** Path string is malformed or contains invalid characters */ -export class InvalidPathError extends Schema.TaggedError()('InvalidPathError', { - /** The path that caused the error */ - path: Schema.String, - /** Human-readable error message */ - message: Schema.String, - /** Specific reason for invalidity */ - reason: InvalidPathReason, - /** Position in path where error was detected (if applicable) */ - position: Schema.UndefinedOr(Schema.Number), -}) {} +export class InvalidPathError extends Schema.TaggedErrorClass()( + 'InvalidPathError', + { + /** The path that caused the error */ + path: Schema.String, + /** Human-readable error message */ + message: Schema.String, + /** Specific reason for invalidity */ + reason: InvalidPathReason, + /** Position in path where error was detected (if applicable) */ + position: Schema.UndefinedOr(Schema.Number), + }, +) {} // ═══════════════════════════════════════════════════════════════════════════ // Absolute/Relative Errors // ═══════════════════════════════════════════════════════════════════════════ /** Expected absolute path but got relative */ -export class NotAbsoluteError extends Schema.TaggedError()('NotAbsoluteError', { - /** The path that caused the error */ - path: Schema.String, - /** Human-readable error message */ - message: Schema.String, - /** Suggestion: resolved absolute path if we can compute it */ - suggestedAbsolute: Schema.UndefinedOr(Schema.String), -}) {} +export class NotAbsoluteError extends Schema.TaggedErrorClass()( + 'NotAbsoluteError', + { + /** The path that caused the error */ + path: Schema.String, + /** Human-readable error message */ + message: Schema.String, + /** Suggestion: resolved absolute path if we can compute it */ + suggestedAbsolute: Schema.UndefinedOr(Schema.String), + }, +) {} /** Expected relative path but got absolute */ -export class NotRelativeError extends Schema.TaggedError()('NotRelativeError', { - /** The path that caused the error */ - path: Schema.String, - /** Human-readable error message */ - message: Schema.String, - /** The absolute prefix that was found (e.g., "/" or "C:\") */ - absolutePrefix: Schema.String, -}) {} +export class NotRelativeError extends Schema.TaggedErrorClass()( + 'NotRelativeError', + { + /** The path that caused the error */ + path: Schema.String, + /** Human-readable error message */ + message: Schema.String, + /** The absolute prefix that was found (e.g., "/" or "C:\") */ + absolutePrefix: Schema.String, + }, +) {} // ═══════════════════════════════════════════════════════════════════════════ // File/Directory Errors // ═══════════════════════════════════════════════════════════════════════════ /** Expected a file but found a directory */ -export class NotAFileError extends Schema.TaggedError()('NotAFileError', { +export class NotAFileError extends Schema.TaggedErrorClass()('NotAFileError', { /** The path that caused the error */ path: Schema.String, /** Human-readable error message */ @@ -72,7 +81,7 @@ export class NotAFileError extends Schema.TaggedError()('NotAFile }) {} /** Expected a directory but found a file */ -export class NotADirectoryError extends Schema.TaggedError()( +export class NotADirectoryError extends Schema.TaggedErrorClass()( 'NotADirectoryError', { /** The path that caused the error */ @@ -85,15 +94,15 @@ export class NotADirectoryError extends Schema.TaggedError() ) {} /** Path does not follow expected convention (trailing slash for dirs, no trailing for files) */ -export class ConventionError extends Schema.TaggedError()('ConventionError', { +export class ConventionError extends Schema.TaggedErrorClass()('ConventionError', { /** The path that caused the error */ path: Schema.String, /** Human-readable error message */ message: Schema.String, /** What was expected */ - expected: Schema.Literal('file', 'directory'), + expected: Schema.Literals(['file', 'directory']), /** What convention violation was found */ - violation: Schema.Literal('trailing_slash_on_file', 'no_trailing_slash_on_directory'), + violation: Schema.Literals(['trailing_slash_on_file', 'no_trailing_slash_on_directory']), }) {} // ═══════════════════════════════════════════════════════════════════════════ @@ -101,7 +110,7 @@ export class ConventionError extends Schema.TaggedError()('Conv // ═══════════════════════════════════════════════════════════════════════════ /** Path does not exist on filesystem */ -export class PathNotFoundError extends Schema.TaggedError()( +export class PathNotFoundError extends Schema.TaggedErrorClass()( 'PathNotFoundError', { /** The path that was not found */ @@ -111,38 +120,44 @@ export class PathNotFoundError extends Schema.TaggedError()( /** The deepest existing ancestor path */ nearestExisting: Schema.UndefinedOr(Schema.String), /** What was expected to exist */ - expectedType: Schema.UndefinedOr(Schema.Literal('file', 'directory', 'any')), + expectedType: Schema.UndefinedOr(Schema.Literals(['file', 'directory', 'any'])), }, ) {} /** Expected a symlink but path is not a symlink */ -export class NotASymlinkError extends Schema.TaggedError()('NotASymlinkError', { - /** The path that was expected to be a symlink */ - path: Schema.String, - /** Human-readable error message */ - message: Schema.String, - /** Actual type found */ - actualType: Schema.Literal('file', 'directory'), -}) {} +export class NotASymlinkError extends Schema.TaggedErrorClass()( + 'NotASymlinkError', + { + /** The path that was expected to be a symlink */ + path: Schema.String, + /** Human-readable error message */ + message: Schema.String, + /** Actual type found */ + actualType: Schema.Literals(['file', 'directory']), + }, +) {} /** Circular symlink detected during resolution */ -export class SymlinkLoopError extends Schema.TaggedError()('SymlinkLoopError', { - /** The path where the loop was detected */ - path: Schema.String, - /** Human-readable error message */ - message: Schema.String, - /** The symlink chain that was followed before loop detected */ - chain: Schema.Array(Schema.String), - /** The symlink that points back to earlier in chain */ - loopingLink: Schema.String, -}) {} +export class SymlinkLoopError extends Schema.TaggedErrorClass()( + 'SymlinkLoopError', + { + /** The path where the loop was detected */ + path: Schema.String, + /** Human-readable error message */ + message: Schema.String, + /** The symlink chain that was followed before loop detected */ + chain: Schema.Array(Schema.String), + /** The symlink that points back to earlier in chain */ + loopingLink: Schema.String, + }, +) {} // ═══════════════════════════════════════════════════════════════════════════ // Security Errors // ═══════════════════════════════════════════════════════════════════════════ /** Path escapes sandbox/jail directory */ -export class TraversalError extends Schema.TaggedError()('TraversalError', { +export class TraversalError extends Schema.TaggedErrorClass()('TraversalError', { /** The path that attempted to escape */ path: Schema.String, /** Human-readable error message */ @@ -156,13 +171,13 @@ export class TraversalError extends Schema.TaggedError()('Traver }) {} /** Permission denied accessing path */ -export class PermissionError extends Schema.TaggedError()('PermissionError', { +export class PermissionError extends Schema.TaggedErrorClass()('PermissionError', { /** The path that could not be accessed */ path: Schema.String, /** Human-readable error message */ message: Schema.String, /** Operation that was attempted */ - operation: Schema.Literal('read', 'write', 'execute', 'stat'), + operation: Schema.Literals(['read', 'write', 'execute', 'stat']), }) {} // ═══════════════════════════════════════════════════════════════════════════ diff --git a/packages/@overeng/effect-path/src/internal/utils.ts b/packages/@overeng/effect-path/src/internal/utils.ts index 889d9b5a95..02a517e76d 100644 --- a/packages/@overeng/effect-path/src/internal/utils.ts +++ b/packages/@overeng/effect-path/src/internal/utils.ts @@ -4,8 +4,7 @@ * Uses @effect/platform's Path service for cross-platform compatibility. */ -import { Path as PlatformPath } from '@effect/platform' -import { Effect } from 'effect' +import { Effect, Path as PlatformPath } from 'effect' // ═══════════════════════════════════════════════════════════════════════════ // Platform Path Access diff --git a/packages/@overeng/effect-path/src/mod.ts b/packages/@overeng/effect-path/src/mod.ts index e81fa8b8e2..d19e3710d0 100644 --- a/packages/@overeng/effect-path/src/mod.ts +++ b/packages/@overeng/effect-path/src/mod.ts @@ -28,7 +28,7 @@ * const joined = EffectPath.ops.join(dir, EffectPath.unsafe.relativeFile('index.ts')) * * // Schema integration - * const parsed = Schema.decodeUnknown(EffectPath.schema.AbsoluteFilePath)(input) + * const parsed = Schema.decodeUnknownEffect(EffectPath.schema.AbsoluteFilePath)(input) * * // Sandbox for secure file access * const sb = EffectPath.sandbox(EffectPath.unsafe.absoluteDir('/app/data/')) diff --git a/packages/@overeng/effect-path/src/mod.unit.test.ts b/packages/@overeng/effect-path/src/mod.unit.test.ts index ef07c2d4be..fbaaa66fef 100644 --- a/packages/@overeng/effect-path/src/mod.unit.test.ts +++ b/packages/@overeng/effect-path/src/mod.unit.test.ts @@ -1,6 +1,5 @@ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' -import { Effect, Either, Schema } from 'effect' +import { NodeServices } from '@effect/platform-node' +import { Effect, FileSystem, Result, Schema } from 'effect' import { expect, it } from 'vitest' import { Vitest } from '@overeng/utils-dev/node-vitest' @@ -23,25 +22,25 @@ Vitest.describe('convention parsing', () => { Effect.gen(function* () { const result = yield* EffectPath.convention.absoluteFile('/home/user/file.txt') expect(result).toBe('/home/user/file.txt') - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) Vitest.it.effect('fails on relative path', () => Effect.gen(function* () { const result = yield* EffectPath.convention .absoluteFile('relative/path.txt') - .pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - }).pipe(Effect.provide(NodeContext.layer)), + .pipe(Effect.result) + expect(Result.isFailure(result)).toBe(true) + }).pipe(Effect.provide(NodeServices.layer)), ) Vitest.it.effect('fails on directory path (trailing slash)', () => Effect.gen(function* () { const result = yield* EffectPath.convention .absoluteFile('/path/to/dir/') - .pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - }).pipe(Effect.provide(NodeContext.layer)), + .pipe(Effect.result) + expect(Result.isFailure(result)).toBe(true) + }).pipe(Effect.provide(NodeServices.layer)), ) }) @@ -50,14 +49,14 @@ Vitest.describe('convention parsing', () => { Effect.gen(function* () { const result = yield* EffectPath.convention.absoluteDir('/home/user/') expect(result).toBe('/home/user/') - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) Vitest.it.effect('fails on file path (no trailing slash)', () => Effect.gen(function* () { - const result = yield* EffectPath.convention.absoluteDir('/path/to/file').pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - }).pipe(Effect.provide(NodeContext.layer)), + const result = yield* EffectPath.convention.absoluteDir('/path/to/file').pipe(Effect.result) + expect(Result.isFailure(result)).toBe(true) + }).pipe(Effect.provide(NodeServices.layer)), ) }) @@ -66,7 +65,7 @@ Vitest.describe('convention parsing', () => { Effect.gen(function* () { const result = yield* EffectPath.convention.relativeFile('src/mod.ts') expect(result).toBe('src/mod.ts') - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) }) @@ -75,7 +74,7 @@ Vitest.describe('convention parsing', () => { Effect.gen(function* () { const result = yield* EffectPath.convention.relativeDir('src/components/') expect(result).toBe('src/components/') - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) }) @@ -86,7 +85,7 @@ Vitest.describe('convention parsing', () => { expect(result.normalized).toBe('/home/user/file.txt') expect(result.baseName).toBe('file') expect(result.extension).toBe('txt') - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) Vitest.it.effect('parses file with multiple extensions', () => @@ -95,7 +94,7 @@ Vitest.describe('convention parsing', () => { expect(result.baseName).toBe('archive') expect(result.extension).toBe('gz') expect(result.fullExtension).toBe('tar.gz') - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) }) @@ -106,7 +105,7 @@ Vitest.describe('convention parsing', () => { expect(result.normalized).toBe('/home/user/') expect(result.baseName).toBe('user') expect(result.extension).toBeUndefined() - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) }) }) @@ -294,7 +293,7 @@ Vitest.describe('normalization', () => { const path = EffectPath.unsafe.absoluteFile('/home/user/../admin/file.txt') const result = yield* EffectPath.normalize.lexical(path) expect(result).toBe('/home/admin/file.txt') - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) }) }) @@ -307,7 +306,7 @@ Vitest.describe('schema', () => { Vitest.describe('AbsoluteFilePath', () => { Vitest.it.effect('decodes valid absolute file path', () => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown(EffectPath.schema.AbsoluteFilePath)( + const result = yield* Schema.decodeUnknownEffect(EffectPath.schema.AbsoluteFilePath)( '/home/user/file.txt', ) expect(result).toBe('/home/user/file.txt') @@ -316,19 +315,19 @@ Vitest.describe('schema', () => { Vitest.it.effect('rejects relative path', () => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown(EffectPath.schema.AbsoluteFilePath)( + const result = yield* Schema.decodeUnknownEffect(EffectPath.schema.AbsoluteFilePath)( 'relative/file.txt', - ).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) + ).pipe(Effect.result) + expect(Result.isFailure(result)).toBe(true) }), ) Vitest.it.effect('rejects directory path', () => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown(EffectPath.schema.AbsoluteFilePath)( + const result = yield* Schema.decodeUnknownEffect(EffectPath.schema.AbsoluteFilePath)( '/path/to/dir/', - ).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) + ).pipe(Effect.result) + expect(Result.isFailure(result)).toBe(true) }), ) }) @@ -336,17 +335,19 @@ Vitest.describe('schema', () => { Vitest.describe('AbsoluteDirPath', () => { Vitest.it.effect('decodes valid absolute directory path', () => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown(EffectPath.schema.AbsoluteDirPath)('/home/user/') + const result = yield* Schema.decodeUnknownEffect(EffectPath.schema.AbsoluteDirPath)( + '/home/user/', + ) expect(result).toBe('/home/user/') }), ) Vitest.it.effect('rejects file path (no trailing slash)', () => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown(EffectPath.schema.AbsoluteDirPath)( + const result = yield* Schema.decodeUnknownEffect(EffectPath.schema.AbsoluteDirPath)( '/home/user/file.txt', - ).pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) + ).pipe(Effect.result) + expect(Result.isFailure(result)).toBe(true) }), ) }) @@ -354,7 +355,9 @@ Vitest.describe('schema', () => { Vitest.describe('RelativeFilePath', () => { Vitest.it.effect('decodes valid relative file path', () => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown(EffectPath.schema.RelativeFilePath)('src/mod.ts') + const result = yield* Schema.decodeUnknownEffect(EffectPath.schema.RelativeFilePath)( + 'src/mod.ts', + ) expect(result).toBe('src/mod.ts') }), ) @@ -363,7 +366,7 @@ Vitest.describe('schema', () => { Vitest.describe('RelativeDirPath', () => { Vitest.it.effect('decodes valid relative directory path', () => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown(EffectPath.schema.RelativeDirPath)( + const result = yield* Schema.decodeUnknownEffect(EffectPath.schema.RelativeDirPath)( 'src/components/', ) expect(result).toBe('src/components/') @@ -374,7 +377,7 @@ Vitest.describe('schema', () => { Vitest.describe('AbsoluteFileInfo', () => { Vitest.it.effect('decodes to PathInfo structure', () => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown(EffectPath.schema.AbsoluteFileInfo())( + const result = yield* Schema.decodeUnknownEffect(EffectPath.schema.AbsoluteFileInfo())( '/path/to/file.txt', ) expect(result.original).toBe('/path/to/file.txt') @@ -385,10 +388,10 @@ Vitest.describe('schema', () => { Vitest.it.effect('encodes back to normalized string by default', () => Effect.gen(function* () { - const decoded = yield* Schema.decodeUnknown(EffectPath.schema.AbsoluteFileInfo())( + const decoded = yield* Schema.decodeUnknownEffect(EffectPath.schema.AbsoluteFileInfo())( '/path/to/file.txt', ) - const encoded = yield* Schema.encode(EffectPath.schema.AbsoluteFileInfo())(decoded) + const encoded = yield* Schema.encodeEffect(EffectPath.schema.AbsoluteFileInfo())(decoded) expect(encoded).toBe('/path/to/file.txt') }), ) @@ -397,7 +400,7 @@ Vitest.describe('schema', () => { Vitest.describe('RelativeFileInfo', () => { Vitest.it.effect('uses ./ as parent for root-level relative files', () => Effect.gen(function* () { - const decoded = yield* Schema.decodeUnknown(EffectPath.schema.RelativeFileInfo())( + const decoded = yield* Schema.decodeUnknownEffect(EffectPath.schema.RelativeFileInfo())( 'file.txt', ) expect(decoded.parent.normalized).toBe('./') @@ -416,59 +419,59 @@ Vitest.describe('sandbox', () => { Vitest.describe('validate', () => { it('validates safe relative path', () => { const result = sandbox.validate(EffectPath.unsafe.relativeFile('file.txt')) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it('validates nested relative path', () => { const result = sandbox.validate(EffectPath.unsafe.relativeFile('sub/dir/file.txt')) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) it('rejects path escaping with ..', () => { const result = sandbox.validate(EffectPath.unsafe.relativeFile('../../../etc/passwd')) - expect(Either.isLeft(result)).toBe(true) - if (Either.isLeft(result) === true) { - expect(result.left._tag).toBe('TraversalError') + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result) === true) { + expect(result.failure._tag).toBe('TraversalError') } }) it('rejects paths that escape and then re-enter', () => { const result = sandbox.validate(EffectPath.unsafe.relativeFile('../safe/file.txt')) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it('rejects absolute paths passed as relative', () => { const result = sandbox.validate(EffectPath.unsafe.relativeFile('/etc/passwd')) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) it('allows .. that stays within sandbox', () => { const result = sandbox.validate(EffectPath.unsafe.relativeFile('sub/../file.txt')) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) }) Vitest.describe('resolve', () => { it('resolves relative path to absolute', () => { const result = sandbox.resolve(EffectPath.unsafe.relativeFile('file.txt')) - expect(Either.isRight(result)).toBe(true) - if (Either.isRight(result) === true) { - expect(result.right).toBe('/home/user/file.txt') + expect(Result.isSuccess(result)).toBe(true) + if (Result.isSuccess(result) === true) { + expect(result.success).toBe('/home/user/file.txt') } }) it('resolves nested path', () => { const result = sandbox.resolve(EffectPath.unsafe.relativeFile('sub/dir/file.txt')) - expect(Either.isRight(result)).toBe(true) - if (Either.isRight(result) === true) { - expect(result.right).toBe('/home/user/sub/dir/file.txt') + expect(Result.isSuccess(result)).toBe(true) + if (Result.isSuccess(result) === true) { + expect(result.success).toBe('/home/user/sub/dir/file.txt') } }) it('rejects escaping path', () => { // Need at least 3 .. to escape /home/user/ (which is 2 levels deep) const result = sandbox.resolve(EffectPath.unsafe.relativeFile('../../../etc/passwd')) - expect(Either.isLeft(result)).toBe(true) + expect(Result.isFailure(result)).toBe(true) }) }) @@ -510,7 +513,7 @@ Vitest.describe('utility functions', () => { const root = EffectPath.unsafe.absoluteDir('/home/user/') const path = EffectPath.unsafe.relativeFile('file.txt') const result = EffectPath.validatePath({ root, path }) - expect(Either.isRight(result)).toBe(true) + expect(Result.isSuccess(result)).toBe(true) }) }) @@ -537,18 +540,18 @@ Vitest.describe('edge cases', () => { Vitest.describe('path validation', () => { Vitest.it.effect('rejects empty path', () => Effect.gen(function* () { - const result = yield* EffectPath.convention.absoluteFile('').pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - }).pipe(Effect.provide(NodeContext.layer)), + const result = yield* EffectPath.convention.absoluteFile('').pipe(Effect.result) + expect(Result.isFailure(result)).toBe(true) + }).pipe(Effect.provide(NodeServices.layer)), ) Vitest.it.effect('rejects path with null byte', () => Effect.gen(function* () { const result = yield* EffectPath.convention .absoluteFile('/path/to\0file.txt') - .pipe(Effect.either) - expect(Either.isLeft(result)).toBe(true) - }).pipe(Effect.provide(NodeContext.layer)), + .pipe(Effect.result) + expect(Result.isFailure(result)).toBe(true) + }).pipe(Effect.provide(NodeServices.layer)), ) }) @@ -605,6 +608,6 @@ Vitest.describe('symlink', () => { const chain = yield* EffectPath.symlink.chain(link) expect(chain).toEqual([link, target]) - }).pipe(Effect.scoped, Effect.provide(NodeContext.layer)), + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ) }) diff --git a/packages/@overeng/effect-path/src/normalize.ts b/packages/@overeng/effect-path/src/normalize.ts index 37bee58285..23d1976c5c 100644 --- a/packages/@overeng/effect-path/src/normalize.ts +++ b/packages/@overeng/effect-path/src/normalize.ts @@ -7,8 +7,7 @@ * 3. Canonical - Full resolution including symlinks (requires FileSystem) */ -import { FileSystem, Path as PlatformPath } from '@effect/platform' -import { Effect } from 'effect' +import { Effect, FileSystem, Path as PlatformPath } from 'effect' import type { AbsolutePath, Path, RelativePath } from './brands.ts' import { PathNotFoundError, SymlinkLoopError } from './errors.ts' @@ -157,7 +156,7 @@ export const canonical = Effect.fnUntraced(function* (path: Path) { const realPath = yield* fs.realPath(absolutePath).pipe( Effect.mapError((error) => { // Map platform errors to our error types - if (error._tag === 'SystemError' && error.reason === 'NotFound') { + if (error.reason._tag === 'NotFound') { return new PathNotFoundError({ path: absolutePath, message: `Path not found: ${absolutePath}`, @@ -198,7 +197,7 @@ export const canonicalOrLexical = Effect.fnUntraced(function* (path: Path) { // Try canonical first const result = yield* canonical(path).pipe( - Effect.orElse(() => + Effect.catch(() => // Fall back to lexical normalization lexical(absolutePath), ), diff --git a/packages/@overeng/effect-path/src/ops.ts b/packages/@overeng/effect-path/src/ops.ts index 7dd8f380f3..df80dbc411 100644 --- a/packages/@overeng/effect-path/src/ops.ts +++ b/packages/@overeng/effect-path/src/ops.ts @@ -5,8 +5,7 @@ * Uses @effect/platform's Path service for cross-platform compatibility. */ -import { Path as PlatformPath } from '@effect/platform' -import { Effect } from 'effect' +import { Effect, Path as PlatformPath } from 'effect' import type { AbsoluteDirPath, diff --git a/packages/@overeng/effect-path/src/schema.ts b/packages/@overeng/effect-path/src/schema.ts index 599d7c5c8d..9776c2fc34 100644 --- a/packages/@overeng/effect-path/src/schema.ts +++ b/packages/@overeng/effect-path/src/schema.ts @@ -5,7 +5,7 @@ * Supports configurable encoding (original vs normalized). */ -import { Schema } from 'effect' +import { Schema, SchemaGetter } from 'effect' import type { AbsoluteDirPath as AbsoluteDirPathType, @@ -44,19 +44,20 @@ import type { /** Validate basic path string (no null bytes, not empty, not too long) */ const PathStringSchema = Schema.String.pipe( - Schema.filter((s) => !isEmpty(s), { message: () => 'Path cannot be empty' }), - Schema.filter((s) => !hasNullByte(s), { - message: () => 'Path cannot contain null bytes', - }), - Schema.filter((s) => s.length <= MAX_PATH_LENGTH, { - message: () => `Path exceeds maximum length of ${MAX_PATH_LENGTH} characters`, - }), - Schema.filter( - (s) => { + Schema.check(Schema.makeFilter((s) => !isEmpty(s) || 'Path cannot be empty')), + Schema.check(Schema.makeFilter((s) => !hasNullByte(s) || 'Path cannot contain null bytes')), + Schema.check( + Schema.makeFilter( + (s) => + s.length <= MAX_PATH_LENGTH || + `Path exceeds maximum length of ${MAX_PATH_LENGTH} characters`, + ), + ), + Schema.check( + Schema.makeFilter((s) => { const segments = toSegments(s) - return !segments.some(isWindowsReservedName) - }, - { message: () => 'Path contains Windows reserved name' }, + return !segments.some(isWindowsReservedName) || 'Path contains Windows reserved name' + }), ), ) @@ -80,51 +81,56 @@ const isAbsoluteHeuristic = (s: string): boolean => { /** Schema for AbsolutePath (absolute, could be file or dir) */ export const AbsolutePath = PathStringSchema.pipe( - Schema.filter(isAbsoluteHeuristic, { - message: () => 'Expected absolute path (starting with / or drive letter)', - }), + Schema.check( + Schema.makeFilter( + (s) => isAbsoluteHeuristic(s) || 'Expected absolute path (starting with / or drive letter)', + ), + ), Schema.brand('Abs'), -) as unknown as Schema.Schema +) as unknown as Schema.Codec /** Schema for RelativePath (relative, could be file or dir) */ export const RelativePath = PathStringSchema.pipe( - Schema.filter((s) => !isAbsoluteHeuristic(s), { - message: () => 'Expected relative path (not starting with / or drive letter)', - }), + Schema.check( + Schema.makeFilter( + (s) => + !isAbsoluteHeuristic(s) || 'Expected relative path (not starting with / or drive letter)', + ), + ), Schema.brand('Rel'), -) as unknown as Schema.Schema +) as unknown as Schema.Codec /** Schema for AbsoluteFilePath (absolute file, no trailing slash) */ export const AbsoluteFilePath = AbsolutePath.pipe( - Schema.filter((s) => !hasTrailingSlash(s), { - message: () => 'File path must not end with a separator', - }), + Schema.check( + Schema.makeFilter((s) => !hasTrailingSlash(s) || 'File path must not end with a separator'), + ), Schema.brand('File'), -) as unknown as Schema.Schema +) as unknown as Schema.Codec /** Schema for AbsoluteDirPath (absolute directory, has trailing slash) */ export const AbsoluteDirPath = AbsolutePath.pipe( - Schema.filter(hasTrailingSlash, { - message: () => 'Directory path must end with a separator', - }), + Schema.check( + Schema.makeFilter((s) => hasTrailingSlash(s) || 'Directory path must end with a separator'), + ), Schema.brand('Dir'), -) as unknown as Schema.Schema +) as unknown as Schema.Codec /** Schema for RelativeFilePath (relative file, no trailing slash) */ export const RelativeFilePath = RelativePath.pipe( - Schema.filter((s) => !hasTrailingSlash(s), { - message: () => 'File path must not end with a separator', - }), + Schema.check( + Schema.makeFilter((s) => !hasTrailingSlash(s) || 'File path must not end with a separator'), + ), Schema.brand('File'), -) as unknown as Schema.Schema +) as unknown as Schema.Codec /** Schema for RelativeDirPath (relative directory, has trailing slash) */ export const RelativeDirPath = RelativePath.pipe( - Schema.filter(hasTrailingSlash, { - message: () => 'Directory path must end with a separator', - }), + Schema.check( + Schema.makeFilter((s) => hasTrailingSlash(s) || 'Directory path must end with a separator'), + ), Schema.brand('Dir'), -) as unknown as Schema.Schema +) as unknown as Schema.Codec // ═══════════════════════════════════════════════════════════════════════════ // PathInfo Schemas @@ -141,15 +147,15 @@ export interface PathInfoSchemaOptions { */ export const AbsoluteFileInfo = ( options?: PathInfoSchemaOptions, -): Schema.Schema => +): Schema.Codec => createPathInfoSchema( options === undefined ? { - baseSchema: AbsoluteFilePath as Schema.Schema, + baseSchema: AbsoluteFilePath as Schema.Codec, isFile: true, } : { - baseSchema: AbsoluteFilePath as Schema.Schema, + baseSchema: AbsoluteFilePath as Schema.Codec, isFile: true, options, }, @@ -160,15 +166,15 @@ export const AbsoluteFileInfo = ( */ export const AbsoluteDirInfo = ( options?: PathInfoSchemaOptions, -): Schema.Schema => +): Schema.Codec => createPathInfoSchema( options === undefined ? { - baseSchema: AbsoluteDirPath as Schema.Schema, + baseSchema: AbsoluteDirPath as Schema.Codec, isFile: false, } : { - baseSchema: AbsoluteDirPath as Schema.Schema, + baseSchema: AbsoluteDirPath as Schema.Codec, isFile: false, options, }, @@ -179,15 +185,15 @@ export const AbsoluteDirInfo = ( */ export const RelativeFileInfo = ( options?: PathInfoSchemaOptions, -): Schema.Schema => +): Schema.Codec => createPathInfoSchema( options === undefined ? { - baseSchema: RelativeFilePath as Schema.Schema, + baseSchema: RelativeFilePath as Schema.Codec, isFile: true, } : { - baseSchema: RelativeFilePath as Schema.Schema, + baseSchema: RelativeFilePath as Schema.Codec, isFile: true, options, }, @@ -198,15 +204,15 @@ export const RelativeFileInfo = ( */ export const RelativeDirInfo = ( options?: PathInfoSchemaOptions, -): Schema.Schema => +): Schema.Codec => createPathInfoSchema( options === undefined ? { - baseSchema: RelativeDirPath as Schema.Schema, + baseSchema: RelativeDirPath as Schema.Codec, isFile: false, } : { - baseSchema: RelativeDirPath as Schema.Schema, + baseSchema: RelativeDirPath as Schema.Codec, isFile: false, options, }, @@ -280,15 +286,20 @@ const buildPathInfoPure = (args: { * Create a schema that transforms a string into PathInfo. */ const createPathInfoSchema = (args: { - readonly baseSchema: Schema.Schema + readonly baseSchema: Schema.Codec readonly isFile: boolean readonly options?: PathInfoSchemaOptions -}): Schema.Schema, string> => { +}): Schema.Codec, string> => { const encodeAs = args.options?.encodeAs ?? 'normalized' - return Schema.transform(args.baseSchema, Schema.Unknown as Schema.Schema>, { - strict: true, - decode: (s) => buildPathInfoPure({ original: s, isFile: args.isFile }), - encode: (info) => (encodeAs === 'original' ? info.original : info.normalized) as string & B & T, - }) + return args.baseSchema.pipe( + Schema.decodeTo(Schema.Unknown as Schema.Codec>, { + decode: SchemaGetter.transform, string & B & T>((s) => + buildPathInfoPure({ original: s, isFile: args.isFile }), + ), + encode: SchemaGetter.transform>( + (info) => (encodeAs === 'original' ? info.original : info.normalized) as string & B & T, + ), + }), + ) } diff --git a/packages/@overeng/effect-path/src/symlink.ts b/packages/@overeng/effect-path/src/symlink.ts index 2ddfe62b27..df35fc5909 100644 --- a/packages/@overeng/effect-path/src/symlink.ts +++ b/packages/@overeng/effect-path/src/symlink.ts @@ -4,8 +4,7 @@ * Provides utilities for detecting, reading, and resolving symbolic links. */ -import { FileSystem, Path as PlatformPath, type Error as PlatformError } from '@effect/platform' -import { Effect, Either } from 'effect' +import { Effect, FileSystem, Path as PlatformPath, type PlatformError, Result } from 'effect' import type { AbsolutePath, Path } from './brands.ts' import { NotASymlinkError, PathNotFoundError, PermissionError, SymlinkLoopError } from './errors.ts' @@ -28,22 +27,20 @@ const mapFsError = (args: { readonly error: PlatformError.PlatformError }): PathNotFoundError | PermissionError => { const { path, error } = args - if (error._tag === 'SystemError') { - if (error.reason === 'NotFound') { - return new PathNotFoundError({ - path, - message: `Path not found: ${path}`, - nearestExisting: undefined, - expectedType: 'any', - }) - } - if (error.reason === 'PermissionDenied') { - return new PermissionError({ - path, - message: `Permission denied: ${path}`, - operation: 'stat', - }) - } + if (error.reason._tag === 'NotFound') { + return new PathNotFoundError({ + path, + message: `Path not found: ${path}`, + nearestExisting: undefined, + expectedType: 'any', + }) + } + if (error.reason._tag === 'PermissionDenied') { + return new PermissionError({ + path, + message: `Permission denied: ${path}`, + operation: 'stat', + }) } return new PathNotFoundError({ path, @@ -68,33 +65,30 @@ export const isSymlink = Effect.fnUntraced(function* (path: AbsolutePath) { const handleReadLinkError = ( error: PlatformError.PlatformError, ): Effect.Effect => { - if (error._tag === 'SystemError') { - if (error.reason === 'NotFound') { - return Effect.fail( - new PathNotFoundError({ - path, - message: `Path not found: ${path}`, - nearestExisting: undefined, - expectedType: 'any', - }), - ) - } - if (error.reason === 'PermissionDenied') { - return Effect.fail( - new PermissionError({ - path, - message: `Permission denied: ${path}`, - operation: 'stat', - }), - ) - } + if (error.reason._tag === 'NotFound') { + return Effect.fail( + new PathNotFoundError({ + path, + message: `Path not found: ${path}`, + nearestExisting: undefined, + expectedType: 'any', + }), + ) + } + if (error.reason._tag === 'PermissionDenied') { + return Effect.fail( + new PermissionError({ + path, + message: `Permission denied: ${path}`, + operation: 'stat', + }), + ) } - // Likely not a symlink (EINVAL), treat as false. return Effect.succeed(false) } - return yield* fs.readLink(path).pipe(Effect.as(true), Effect.catchAll(handleReadLinkError)) + return yield* fs.readLink(path).pipe(Effect.as(true), Effect.catch(handleReadLinkError)) }) /** @@ -103,7 +97,7 @@ export const isSymlink = Effect.fnUntraced(function* (path: AbsolutePath) { export const isSymlinkSafe = ( path: AbsolutePath, ): Effect.Effect => - isSymlink(path).pipe(Effect.orElse(() => Effect.succeed(false))) + isSymlink(path).pipe(Effect.catch(() => Effect.succeed(false))) // ═══════════════════════════════════════════════════════════════════════════ // Symlink Reading @@ -117,42 +111,39 @@ export const isSymlinkSafe = ( export const readLink = Effect.fnUntraced(function* (path: AbsolutePath) { const fs = yield* FileSystem.FileSystem - const targetResult = yield* fs.readLink(path).pipe(Effect.either) - if (Either.isRight(targetResult) === true) { - return targetResult.right as Path + const targetResult = yield* fs.readLink(path).pipe(Effect.result) + if (Result.isSuccess(targetResult) === true) { + return targetResult.success as Path } - const error = targetResult.left - if (error._tag === 'SystemError') { - if (error.reason === 'NotFound') { - return yield* new PathNotFoundError({ - path, - message: `Path not found: ${path}`, - nearestExisting: undefined, - expectedType: 'any', - }) - } - if (error.reason === 'PermissionDenied') { - return yield* new PermissionError({ - path, - message: `Permission denied: ${path}`, - operation: 'stat', - }) - } + const error = targetResult.failure + if (error.reason._tag === 'NotFound') { + return yield* new PathNotFoundError({ + path, + message: `Path not found: ${path}`, + nearestExisting: undefined, + expectedType: 'any', + }) + } + if (error.reason._tag === 'PermissionDenied') { + return yield* new PermissionError({ + path, + message: `Permission denied: ${path}`, + operation: 'stat', + }) } - const statResult = yield* fs.stat(path).pipe( Effect.mapError((fsError) => mapFsError({ path, error: fsError })), - Effect.either, + Effect.result, ) - if (Either.isLeft(statResult) === true) { - return yield* statResult.left + if (Result.isFailure(statResult) === true) { + return yield* statResult.failure } return yield* new NotASymlinkError({ path, message: `Path is not a symbolic link: ${path}`, - actualType: statResult.right.type === 'Directory' ? 'directory' : 'file', + actualType: statResult.success.type === 'Directory' ? 'directory' : 'file', }) }) @@ -174,22 +165,20 @@ export const resolve = Effect.fnUntraced(function* (path: AbsolutePath) { // Use realPath for efficient symlink resolution const realPath = yield* fs.realPath(path).pipe( Effect.mapError((error) => { - if (error._tag === 'SystemError') { - if (error.reason === 'NotFound') { - return new PathNotFoundError({ - path, - message: `Path not found: ${path}`, - nearestExisting: undefined, - expectedType: 'any', - }) - } - if (error.reason === 'PermissionDenied') { - return new PermissionError({ - path, - message: `Permission denied: ${path}`, - operation: 'stat', - }) - } + if (error.reason._tag === 'NotFound') { + return new PathNotFoundError({ + path, + message: `Path not found: ${path}`, + nearestExisting: undefined, + expectedType: 'any', + }) + } + if (error.reason._tag === 'PermissionDenied') { + return new PermissionError({ + path, + message: `Permission denied: ${path}`, + operation: 'stat', + }) } // ELOOP or other errors return new SymlinkLoopError({ @@ -223,13 +212,10 @@ export const chain = Effect.fnUntraced(function* (path: AbsolutePath) { let current = path for (let depth = 0; depth < MAX_SYMLINK_DEPTH; depth++) { - const targetResult = yield* fs.readLink(current).pipe(Effect.either) - if (Either.isLeft(targetResult) === true) { - const error = targetResult.left - if ( - error._tag === 'SystemError' && - (error.reason === 'NotFound' || error.reason === 'PermissionDenied') - ) { + const targetResult = yield* fs.readLink(current).pipe(Effect.result) + if (Result.isFailure(targetResult) === true) { + const error = targetResult.failure + if (error.reason._tag === 'NotFound' || error.reason._tag === 'PermissionDenied') { return yield* mapFsError({ path: current, error }) } @@ -237,7 +223,7 @@ export const chain = Effect.fnUntraced(function* (path: AbsolutePath) { return visited } - const target = targetResult.right + const target = targetResult.success // Resolve to absolute if relative const absoluteTarget = @@ -277,4 +263,4 @@ export const chain = Effect.fnUntraced(function* (path: AbsolutePath) { export const resolveSafe = ( path: AbsolutePath, ): Effect.Effect => - resolve(path).pipe(Effect.orElse(() => Effect.succeed(path))) + resolve(path).pipe(Effect.catch(() => Effect.succeed(path))) diff --git a/packages/@overeng/effect-path/src/verified.ts b/packages/@overeng/effect-path/src/verified.ts index 673e612952..49648eb2e3 100644 --- a/packages/@overeng/effect-path/src/verified.ts +++ b/packages/@overeng/effect-path/src/verified.ts @@ -5,8 +5,7 @@ * the actual filesystem to ensure they exist and are of the correct type. */ -import { FileSystem, Path as PlatformPath, type Error as PlatformError } from '@effect/platform' -import { Effect } from 'effect' +import { Effect, FileSystem, Path as PlatformPath, type PlatformError } from 'effect' import type { Abs, @@ -104,22 +103,20 @@ const mapFsError = (args: { readonly error: PlatformError.PlatformError }): PathNotFoundError | PermissionError => { const { path, error } = args - if (error._tag === 'SystemError') { - if (error.reason === 'NotFound') { - return new PathNotFoundError({ - path, - message: `Path not found: ${path}`, - nearestExisting: undefined, - expectedType: 'any', - }) - } - if (error.reason === 'PermissionDenied') { - return new PermissionError({ - path, - message: `Permission denied: ${path}`, - operation: 'stat', - }) - } + if (error.reason._tag === 'NotFound') { + return new PathNotFoundError({ + path, + message: `Path not found: ${path}`, + nearestExisting: undefined, + expectedType: 'any', + }) + } + if (error.reason._tag === 'PermissionDenied') { + return new PermissionError({ + path, + message: `Permission denied: ${path}`, + operation: 'stat', + }) } // Default to not found return new PathNotFoundError({ diff --git a/packages/@overeng/effect-react/package.json b/packages/@overeng/effect-react/package.json index 73b67c8ee9..1504ec1884 100644 --- a/packages/@overeng/effect-react/package.json +++ b/packages/@overeng/effect-react/package.json @@ -28,7 +28,7 @@ "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.2", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-aria-components": "1.19.0", "react-dom": "19.2.7", @@ -38,7 +38,7 @@ "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "react": "^19.2.7", "react-aria-components": "^1.19.0", "react-dom": "^19.2.7" diff --git a/packages/@overeng/effect-react/package.json.genie.ts b/packages/@overeng/effect-react/package.json.genie.ts index 1d03d5f73f..9bee9ff128 100644 --- a/packages/@overeng/effect-react/package.json.genie.ts +++ b/packages/@overeng/effect-react/package.json.genie.ts @@ -19,18 +19,7 @@ const workspaceDeps = catalog.compose({ devDependencies: { workspace: [utilsPkg], external: { - ...catalog.pick( - ...peerDepNames, - '@storybook/react', - '@storybook/react-vite', - '@types/react', - '@types/react-dom', - '@vitejs/plugin-react', - 'typescript', - 'storybook', - 'vite', - 'vitest', - ), + ...catalog.pick(...peerDepNames, '@storybook/react', '@storybook/react-vite', '@types/react', '@types/react-dom', '@vitejs/plugin-react', 'typescript', 'storybook', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/effect-react/src/context.tsx b/packages/@overeng/effect-react/src/context.tsx index da08e68fce..bd5ab589a3 100644 --- a/packages/@overeng/effect-react/src/context.tsx +++ b/packages/@overeng/effect-react/src/context.tsx @@ -6,6 +6,7 @@ import { Fiber, type Layer, ManagedRuntime, + Result, Runtime, Schema, type Scope, @@ -305,7 +306,7 @@ const defaultErrorHandler: ErrorHandler = (cause) => { * Extract a user-friendly error message from a Cause. */ export const extractErrorMessage = (cause: Cause.Cause): string => { - const failure = Cause.failureOption(cause) + const failure = Cause.findErrorOption(cause) if (failure._tag === 'Some') { const err = failure.value if ( @@ -319,9 +320,9 @@ export const extractErrorMessage = (cause: Cause.Cause): string => { if (typeof err === 'string') return err return String(err) } - const defect = Cause.dieOption(cause) - if (defect._tag === 'Some') { - const d = defect.value + const defect = Cause.findDefect(cause) + if (Result.isSuccess(defect) === true) { + const d = defect.success if (d instanceof Error) return d.message return String(d) } diff --git a/packages/@overeng/effect-react/src/progress-reporter.ts b/packages/@overeng/effect-react/src/progress-reporter.ts index 6fc7b366a2..f2ec384314 100644 --- a/packages/@overeng/effect-react/src/progress-reporter.ts +++ b/packages/@overeng/effect-react/src/progress-reporter.ts @@ -18,7 +18,7 @@ export const initialProgress: Progress = { total: 0, completed: 0 } /** * Service for reporting progress during Effect execution. */ -export class ProgressReporter extends Context.Tag('ProgressReporter')< +export class ProgressReporter extends Context.Service< ProgressReporter, { /** Set progress to a specific state. */ @@ -26,7 +26,7 @@ export class ProgressReporter extends Context.Tag('ProgressReporter')< /** Stream of progress changes. */ readonly changes: SubscriptionRef.SubscriptionRef['changes'] } ->() { +>()('ProgressReporter') { /** * Set progress to a specific state. */ diff --git a/packages/@overeng/effect-react/test/runtime-baseline.test.tsx b/packages/@overeng/effect-react/test/runtime-baseline.test.tsx index d0fe1f7e5f..eea21ee9c7 100644 --- a/packages/@overeng/effect-react/test/runtime-baseline.test.tsx +++ b/packages/@overeng/effect-react/test/runtime-baseline.test.tsx @@ -30,10 +30,9 @@ afterEach(() => { } }) -class ProbeService extends Context.Tag('ProbeService')< - ProbeService, - { readonly label: string } ->() {} +class ProbeService extends Context.Service()( + 'ProbeService', +) {} const flushReactEffects = async (): Promise => { await act(async () => { diff --git a/packages/@overeng/effect-rpc-tanstack/examples/basic/package.json b/packages/@overeng/effect-rpc-tanstack/examples/basic/package.json index ccba2a6d57..9c4a8e7ce5 100644 --- a/packages/@overeng/effect-rpc-tanstack/examples/basic/package.json +++ b/packages/@overeng/effect-rpc-tanstack/examples/basic/package.json @@ -10,11 +10,9 @@ "test:e2e": "playwright test" }, "dependencies": { - "@effect/platform": "0.96.2", - "@effect/rpc": "0.75.1", "@tanstack/react-router": "1.170.16", "@tanstack/react-start": "1.168.26", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-dom": "19.2.7" }, diff --git a/packages/@overeng/effect-rpc-tanstack/examples/basic/package.json.genie.ts b/packages/@overeng/effect-rpc-tanstack/examples/basic/package.json.genie.ts index 1cbdd9f4ae..df6d060e4e 100644 --- a/packages/@overeng/effect-rpc-tanstack/examples/basic/package.json.genie.ts +++ b/packages/@overeng/effect-rpc-tanstack/examples/basic/package.json.genie.ts @@ -14,30 +14,13 @@ const workspaceDeps = catalog.compose({ }), dependencies: { external: { - ...catalog.pick( - '@effect/platform', - '@effect/rpc', - '@tanstack/react-router', - '@tanstack/react-start', - 'effect', - 'react', - 'react-dom', - ), + ...catalog.pick('effect', '@tanstack/react-router', '@tanstack/react-start', 'react', 'react-dom'), }, }, devDependencies: { workspace: [utilsPkg], external: { - ...catalog.pick( - '@playwright/test', - '@tanstack/router-plugin', - '@types/node', - '@types/react', - '@types/react-dom', - '@vitejs/plugin-react', - 'typescript', - 'vite', - ), + ...catalog.pick('@playwright/test', '@tanstack/router-plugin', '@types/node', '@types/react', '@types/react-dom', '@vitejs/plugin-react', 'typescript', 'vite'), }, }, }) diff --git a/packages/@overeng/effect-rpc-tanstack/examples/basic/src/routes/index.tsx b/packages/@overeng/effect-rpc-tanstack/examples/basic/src/routes/index.tsx index 6964887800..51911ca1c2 100644 --- a/packages/@overeng/effect-rpc-tanstack/examples/basic/src/routes/index.tsx +++ b/packages/@overeng/effect-rpc-tanstack/examples/basic/src/routes/index.tsx @@ -1,4 +1,4 @@ -import type { RpcClientError } from '@effect/rpc' +import type { RpcClientError } from 'effect/unstable/rpc' import { Effect } from 'effect' import { useState, type SyntheticEvent } from 'react' diff --git a/packages/@overeng/effect-rpc-tanstack/examples/basic/src/routes/users.$id.tsx b/packages/@overeng/effect-rpc-tanstack/examples/basic/src/routes/users.$id.tsx index e9a2c2b3cc..0c1dde78dd 100644 --- a/packages/@overeng/effect-rpc-tanstack/examples/basic/src/routes/users.$id.tsx +++ b/packages/@overeng/effect-rpc-tanstack/examples/basic/src/routes/users.$id.tsx @@ -1,4 +1,4 @@ -import type { RpcClientError } from '@effect/rpc' +import type { RpcClientError } from 'effect/unstable/rpc' import { createEffectRoute, diff --git a/packages/@overeng/effect-rpc-tanstack/examples/basic/src/rpc/api.ts b/packages/@overeng/effect-rpc-tanstack/examples/basic/src/rpc/api.ts index 27b98092a0..d0c4c27c2d 100644 --- a/packages/@overeng/effect-rpc-tanstack/examples/basic/src/rpc/api.ts +++ b/packages/@overeng/effect-rpc-tanstack/examples/basic/src/rpc/api.ts @@ -2,8 +2,8 @@ * Shared RPC API definitions using idiomatic @effect/rpc patterns */ -import { Rpc, RpcGroup } from '@effect/rpc' import { Schema } from 'effect' +import { Rpc, RpcGroup } from 'effect/unstable/rpc' /** * User domain model @@ -17,7 +17,7 @@ export class User extends Schema.Class('User')({ /** * Error when user is not found */ -export class UserNotFoundError extends Schema.TaggedError()( +export class UserNotFoundError extends Schema.TaggedErrorClass()( 'UserNotFoundError', { userId: Schema.String, diff --git a/packages/@overeng/effect-rpc-tanstack/examples/basic/src/rpc/client.ts b/packages/@overeng/effect-rpc-tanstack/examples/basic/src/rpc/client.ts index 43ce14fbab..07802655f0 100644 --- a/packages/@overeng/effect-rpc-tanstack/examples/basic/src/rpc/client.ts +++ b/packages/@overeng/effect-rpc-tanstack/examples/basic/src/rpc/client.ts @@ -2,7 +2,7 @@ * Client-side RPC client using idiomatic @effect/rpc patterns */ -import { RpcClient, type RpcClientError } from '@effect/rpc' +import { RpcClient, type RpcClientError } from 'effect/unstable/rpc' import { Effect } from 'effect' import { layerClient } from '../../../../src/client.ts' diff --git a/packages/@overeng/effect-rpc-tanstack/package.json b/packages/@overeng/effect-rpc-tanstack/package.json index e5e795e1d1..0e98de043e 100644 --- a/packages/@overeng/effect-rpc-tanstack/package.json +++ b/packages/@overeng/effect-rpc-tanstack/package.json @@ -19,16 +19,12 @@ } }, "devDependencies": { - "@effect/experimental": "0.60.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/sql": "0.51.1", + "@effect/platform-node": "4.0.0-beta.102", "@overeng/utils": "workspace:^", "@tanstack/react-router": "1.170.16", "@tanstack/react-start": "1.168.26", "@types/react": "19.2.17", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-dom": "19.2.7", "typescript": "6.0.3", @@ -36,12 +32,10 @@ "vitest": "4.1.9" }, "peerDependencies": { - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/rpc": "^0.75.1", + "@effect/platform-node": "^4.0.0-beta.102", "@tanstack/react-router": "^1.170.16", "@tanstack/react-start": "^1.168.26", - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "react": "^19.2.7", "react-dom": "^19.2.7" }, diff --git a/packages/@overeng/effect-rpc-tanstack/package.json.genie.ts b/packages/@overeng/effect-rpc-tanstack/package.json.genie.ts index 96bc1428cd..11b06a16d2 100644 --- a/packages/@overeng/effect-rpc-tanstack/package.json.genie.ts +++ b/packages/@overeng/effect-rpc-tanstack/package.json.genie.ts @@ -9,16 +9,7 @@ import { } from '../../../genie/internal.ts' import utilsPkg from '../utils/package.json.genie.ts' -const peerDepNames = [ - '@effect/platform', - '@effect/platform-node', - '@effect/rpc', - '@tanstack/react-router', - '@tanstack/react-start', - 'effect', - 'react', - 'react-dom', -] as const +const peerDepNames = ['effect', '@effect/platform-node', '@tanstack/react-router', '@tanstack/react-start', 'react', 'react-dom'] as const const workspaceDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/effect-rpc-tanstack', @@ -29,15 +20,7 @@ const workspaceDeps = catalog.compose({ devDependencies: { workspace: [utilsPkg], external: { - ...catalog.pick( - ...peerDepNames, - '@effect/experimental', - '@effect/sql', - '@types/react', - 'typescript', - 'vite', - 'vitest', - ), + ...catalog.pick(...peerDepNames, 'effect', '@types/react', 'typescript', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/effect-rpc-tanstack/src/client.test.ts b/packages/@overeng/effect-rpc-tanstack/src/client.test.ts index e451a06c32..7f227ee167 100644 --- a/packages/@overeng/effect-rpc-tanstack/src/client.test.ts +++ b/packages/@overeng/effect-rpc-tanstack/src/client.test.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup, RpcClient } from '@effect/rpc' +import { Rpc, RpcGroup, RpcClient } from 'effect/unstable/rpc' import { Chunk, Effect, Schema, Stream } from 'effect' import { describe, expect, it, vi } from 'vitest' diff --git a/packages/@overeng/effect-rpc-tanstack/src/client.ts b/packages/@overeng/effect-rpc-tanstack/src/client.ts index a245e952ff..7ca4359ad2 100644 --- a/packages/@overeng/effect-rpc-tanstack/src/client.ts +++ b/packages/@overeng/effect-rpc-tanstack/src/client.ts @@ -4,10 +4,10 @@ * @since 0.1.0 */ -import { FetchHttpClient } from '@effect/platform' -import type * as HttpClient from '@effect/platform/HttpClient' -import { RpcClient, RpcSerialization } from '@effect/rpc' import { Layer } from 'effect' +import { FetchHttpClient } from 'effect/unstable/http' +import type * as HttpClient from 'effect/unstable/http/HttpClient' +import { RpcClient, RpcSerialization } from 'effect/unstable/rpc' type FetchLike = typeof globalThis.fetch diff --git a/packages/@overeng/effect-rpc-tanstack/src/mod.ts b/packages/@overeng/effect-rpc-tanstack/src/mod.ts index 3f2f12b186..d582eee64b 100644 --- a/packages/@overeng/effect-rpc-tanstack/src/mod.ts +++ b/packages/@overeng/effect-rpc-tanstack/src/mod.ts @@ -67,4 +67,4 @@ export * from './router.ts' /** * Re-export RPC primitives for direct access */ -export { RpcClient, RpcClientError, RpcSerialization, RpcServer } from '@effect/rpc' +export { RpcClient, RpcClientError, RpcSerialization, RpcServer } from 'effect/unstable/rpc' diff --git a/packages/@overeng/effect-rpc-tanstack/src/router.ts b/packages/@overeng/effect-rpc-tanstack/src/router.ts index 8969dd7198..6e87eba6e6 100644 --- a/packages/@overeng/effect-rpc-tanstack/src/router.ts +++ b/packages/@overeng/effect-rpc-tanstack/src/router.ts @@ -19,7 +19,7 @@ import { Cause, Effect, Exit, type Layer, Option, Schema } from 'effect' const ExitSchema = Schema.Exit({ success: Schema.Unknown, failure: Schema.Unknown, - defect: Schema.Defect, + defect: Schema.Defect(), }) /** @@ -137,7 +137,7 @@ export const makeEffectLoaderResult = (encoded: ExitEncoded): EffectLoader isSuccess: Exit.isSuccess(exit), isFailure: Exit.isFailure(exit), value: Exit.isSuccess(exit) === true ? Option.some(exit.value) : Option.none(), - error: Exit.isFailure(exit) === true ? Cause.failureOption(exit.cause) : Option.none(), + error: Exit.isFailure(exit) === true ? Cause.findErrorOption(exit.cause) : Option.none(), getOrThrow: () => { if (Exit.isSuccess(exit) === true) { return exit.value @@ -148,7 +148,7 @@ export const makeEffectLoaderResult = (encoded: ExitEncoded): EffectLoader Exit.match(exit, { onSuccess: handlers.onSuccess, onFailure: (cause) => { - const failure = Cause.failureOption(cause) + const failure = Cause.findErrorOption(cause) if (Option.isSome(failure) === true) { return handlers.onFailure(failure.value) } diff --git a/packages/@overeng/effect-rpc-tanstack/src/rpc-wire-baseline.test.ts b/packages/@overeng/effect-rpc-tanstack/src/rpc-wire-baseline.test.ts index fff763b053..ff6b7c3191 100644 --- a/packages/@overeng/effect-rpc-tanstack/src/rpc-wire-baseline.test.ts +++ b/packages/@overeng/effect-rpc-tanstack/src/rpc-wire-baseline.test.ts @@ -1,11 +1,11 @@ -import { Rpc, RpcGroup } from '@effect/rpc' import { Effect, Exit, Schema } from 'effect' +import { Rpc, RpcGroup } from 'effect/unstable/rpc' import { describe, expect, it } from 'vitest' import { encodeExit, decodeExit, makeEffectLoaderResult } from './router.ts' import { makeHandler } from './server.ts' -class GreetingError extends Schema.TaggedError()('GreetingError', { +class GreetingError extends Schema.TaggedErrorClass()('GreetingError', { reason: Schema.Literal('missing-user', 'blocked'), userId: Schema.String, }) {} diff --git a/packages/@overeng/effect-rpc-tanstack/src/server.ts b/packages/@overeng/effect-rpc-tanstack/src/server.ts index ee9cf349b9..89216817b7 100644 --- a/packages/@overeng/effect-rpc-tanstack/src/server.ts +++ b/packages/@overeng/effect-rpc-tanstack/src/server.ts @@ -5,9 +5,9 @@ */ import { NodeHttpServer } from '@effect/platform-node' -import type * as HttpApp from '@effect/platform/HttpApp' -import type * as HttpRouter from '@effect/platform/HttpRouter' -import { type Rpc, type RpcGroup, RpcSerialization, RpcServer } from '@effect/rpc' +import type * as HttpApp from 'effect/unstable/http/HttpApp' +import type * as HttpRouter from 'effect/unstable/http/HttpRouter' +import { type Rpc, type RpcGroup, RpcSerialization, RpcServer } from 'effect/unstable/rpc' import type * as Context from 'effect/Context' import * as Layer from 'effect/Layer' diff --git a/packages/@overeng/effect-rpc-tanstack/src/streaming.unit.test.ts b/packages/@overeng/effect-rpc-tanstack/src/streaming.unit.test.ts index 0b3fabd18d..7a0a0f32b1 100644 --- a/packages/@overeng/effect-rpc-tanstack/src/streaming.unit.test.ts +++ b/packages/@overeng/effect-rpc-tanstack/src/streaming.unit.test.ts @@ -1,4 +1,4 @@ -import { Rpc, RpcGroup } from '@effect/rpc' +import { Rpc, RpcGroup } from 'effect/unstable/rpc' import { Effect, Schema } from 'effect' import * as Stream from 'effect/Stream' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/effect-schema-form-aria/package.json b/packages/@overeng/effect-schema-form-aria/package.json index 5e6fb38296..eba4c95e8a 100644 --- a/packages/@overeng/effect-schema-form-aria/package.json +++ b/packages/@overeng/effect-schema-form-aria/package.json @@ -27,7 +27,7 @@ "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.2", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-aria-components": "1.19.0", "react-dom": "19.2.7", @@ -38,7 +38,7 @@ "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "react": "^19.2.7", "react-aria-components": "^1.19.0", "react-dom": "^19.2.7" diff --git a/packages/@overeng/effect-schema-form-aria/package.json.genie.ts b/packages/@overeng/effect-schema-form-aria/package.json.genie.ts index d23f2d5097..4b8f202ce0 100644 --- a/packages/@overeng/effect-schema-form-aria/package.json.genie.ts +++ b/packages/@overeng/effect-schema-form-aria/package.json.genie.ts @@ -18,22 +18,7 @@ const runtimeDeps = catalog.compose({ devDependencies: { workspace: [utilsPkg], external: { - ...catalog.pick( - ...peerDepNames, - 'effect', - 'react', - '@storybook/react', - '@storybook/react-vite', - '@tailwindcss/vite', - '@types/react', - '@types/react-dom', - '@vitejs/plugin-react', - 'storybook', - 'tailwindcss', - 'typescript', - 'vite', - 'vitest', - ), + ...catalog.pick(...peerDepNames, 'effect', 'react', '@storybook/react', '@storybook/react-vite', '@tailwindcss/vite', '@types/react', '@types/react-dom', '@vitejs/plugin-react', 'storybook', 'tailwindcss', 'typescript', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/effect-schema-form-aria/src/AriaSchemaForm.tsx b/packages/@overeng/effect-schema-form-aria/src/AriaSchemaForm.tsx index 15aa305856..2d8049a41e 100644 --- a/packages/@overeng/effect-schema-form-aria/src/AriaSchemaForm.tsx +++ b/packages/@overeng/effect-schema-form-aria/src/AriaSchemaForm.tsx @@ -37,9 +37,9 @@ export interface AriaSchemaFormProps> { * import { AriaSchemaForm } from '@overeng/effect-schema-form-aria' * * const UserSchema = Schema.Struct({ - * name: Schema.String.annotations({ title: 'Name' }), - * age: Schema.optional(Schema.Number).annotations({ title: 'Age' }), - * role: Schema.Literal('admin', 'user', 'guest'), + * name: Schema.String.annotate({ title: 'Name' }), + * age: Schema.optional(Schema.Number).annotate({ title: 'Age' }), + * role: Schema.Literals(['admin', 'user', 'guest']), * }) * * { description: undefined, literals: undefined, isOptional: false, - innerSchema: Schema.Tuple(Schema.String), + innerSchema: Schema.Tuple([Schema.String]), }} /> @@ -143,11 +143,11 @@ describe('effect-schema-form-aria baselines (cross-major invariant)', () => { it('renders a complete tagged schema form as byte-identical markup', () => { const FormSchema = Schema.TaggedStruct('contact_preferences', { - name: Schema.String.annotations({ title: 'Name', description: 'Unicode accepted' }), - count: Schema.optional(Schema.Number).annotations({ title: 'Count' }), + name: Schema.String.annotate({ title: 'Name', description: 'Unicode accepted' }), + count: Schema.optional(Schema.Number).annotate({ title: 'Count' }), enabled: Schema.Boolean, - mode: Schema.Literal('email', 'push-notification'), - items: Schema.Tuple(Schema.String), + mode: Schema.Literals(['email', 'push-notification']), + items: Schema.Tuple([Schema.String]), }) const html = renderToStaticMarkup( diff --git a/packages/@overeng/effect-schema-form-aria/src/stories/AriaSchemaForm.stories.tsx b/packages/@overeng/effect-schema-form-aria/src/stories/AriaSchemaForm.stories.tsx index d8c14456ab..d067ec48dd 100644 --- a/packages/@overeng/effect-schema-form-aria/src/stories/AriaSchemaForm.stories.tsx +++ b/packages/@overeng/effect-schema-form-aria/src/stories/AriaSchemaForm.stories.tsx @@ -15,16 +15,16 @@ export default { // ============================================================================ const UserSchema = Schema.Struct({ - name: Schema.String.annotations({ + name: Schema.String.annotate({ title: 'Name', description: 'Your full name', }), - email: Schema.String.annotations({ title: 'Email' }), - age: Schema.optional(Schema.Number).annotations({ + email: Schema.String.annotate({ title: 'Email' }), + age: Schema.optional(Schema.Number).annotate({ title: 'Age', description: 'Optional', }), - role: Schema.Literal('admin', 'user', 'guest').annotations({ title: 'Role' }), + role: Schema.Literals(['admin', 'user', 'guest']).annotate({ title: 'Role' }), }) type User = typeof UserSchema.Type @@ -63,11 +63,11 @@ export const WithValues: StoryObj> = { // ============================================================================ const LinkedInSchema = Schema.TaggedStruct('linkedin-contacts', { - includeConnections: Schema.optional(Schema.Boolean).annotations({ + includeConnections: Schema.optional(Schema.Boolean).annotate({ title: 'Include Connections', description: 'Import your LinkedIn connections', }), - syncFrequency: Schema.Literal('hourly', 'daily', 'weekly').annotations({ + syncFrequency: Schema.Literals(['hourly', 'daily', 'weekly']).annotate({ title: 'Sync Frequency', }), }) @@ -133,12 +133,12 @@ export const EmptyTaggedStruct: StoryObj> = // ============================================================================ const SettingsSchema = Schema.Struct({ - darkMode: Schema.Boolean.annotations({ + darkMode: Schema.Boolean.annotate({ title: 'Dark Mode', description: 'Enable dark theme', }), - notifications: Schema.Boolean.annotations({ title: 'Notifications' }), - language: Schema.Literal('en', 'es', 'fr', 'de').annotations({ + notifications: Schema.Boolean.annotate({ title: 'Notifications' }), + language: Schema.Literals(['en', 'es', 'fr', 'de']).annotate({ title: 'Language', }), }) @@ -164,7 +164,7 @@ export const SettingsForm: StoryObj> = { // ============================================================================ const CountrySchema = Schema.Struct({ - country: Schema.Literal('us', 'uk', 'de', 'fr', 'es', 'it', 'nl', 'be', 'at', 'ch').annotations({ + country: Schema.Literals(['us', 'uk', 'de', 'fr', 'es', 'it', 'nl', 'be', 'at', 'ch']).annotate({ title: 'Country', description: 'Select your country', }), @@ -189,28 +189,28 @@ export const DropdownField: StoryObj> = { // ============================================================================ const ComplexSchema = Schema.Struct({ - firstName: Schema.String.annotations({ title: 'First Name' }), - lastName: Schema.String.annotations({ title: 'Last Name' }), - email: Schema.String.annotations({ + firstName: Schema.String.annotate({ title: 'First Name' }), + lastName: Schema.String.annotate({ title: 'Last Name' }), + email: Schema.String.annotate({ title: 'Email Address', description: 'Primary contact email', }), - age: Schema.optional(Schema.Number).annotations({ title: 'Age' }), - subscribe: Schema.Boolean.annotations({ + age: Schema.optional(Schema.Number).annotate({ title: 'Age' }), + subscribe: Schema.Boolean.annotate({ title: 'Subscribe to newsletter', description: 'Receive weekly updates', }), - plan: Schema.Literal('free', 'pro', 'enterprise').annotations({ + plan: Schema.Literals(['free', 'pro', 'enterprise']).annotate({ title: 'Plan', }), - region: Schema.Literal( + region: Schema.Literals([ 'us-east', 'us-west', 'eu-west', 'eu-central', 'ap-south', 'ap-east', - ).annotations({ + ]).annotate({ title: 'Region', description: 'Primary deployment region', }), diff --git a/packages/@overeng/effect-schema-form/package.json b/packages/@overeng/effect-schema-form/package.json index 001edfa51c..ce29d022ba 100644 --- a/packages/@overeng/effect-schema-form/package.json +++ b/packages/@overeng/effect-schema-form/package.json @@ -15,14 +15,14 @@ "devDependencies": { "@types/react": "19.2.17", "@types/react-dom": "19.2.3", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-dom": "19.2.7", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "react": "^19.2.7" }, "$genie": { diff --git a/packages/@overeng/effect-schema-form/package.json.genie.ts b/packages/@overeng/effect-schema-form/package.json.genie.ts index 7bfe8e526c..f06e19be89 100644 --- a/packages/@overeng/effect-schema-form/package.json.genie.ts +++ b/packages/@overeng/effect-schema-form/package.json.genie.ts @@ -13,14 +13,7 @@ const deps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/effect-schema-form' }), devDependencies: { external: { - ...catalog.pick( - ...peerDepNames, - '@types/react', - '@types/react-dom', - 'react-dom', - 'typescript', - 'vitest', - ), + ...catalog.pick(...peerDepNames, '@types/react', '@types/react-dom', 'react-dom', 'typescript', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/effect-schema-form/src/introspection.ts b/packages/@overeng/effect-schema-form/src/introspection.ts index cd843f4340..43d563618d 100644 --- a/packages/@overeng/effect-schema-form/src/introspection.ts +++ b/packages/@overeng/effect-schema-form/src/introspection.ts @@ -25,17 +25,17 @@ * | `NumberKeyword` | `Schema.Number` | TextField type="number" | * | `NumberKeyword` | `Schema.optional(Schema.Number)` | Checkbox toggle + number input | * | `BooleanKeyword` | `Schema.Boolean` | Checkbox | - * | `Literal` | `Schema.Literal('a', 'b')` | SegmentedControl/Select | + * | `Literal` | `Schema.Literals(['a', 'b'])` | SegmentedControl/Select | * | `TypeLiteral` | `Schema.Struct({...})` | Nested field group | * | `Refinement` | `Schema.Int`, `Schema.nonNegative()` | Unwrapped to base type | * | `Transformation` | `Schema.optional()`, `Schema.DateFromSelf` | Unwrapped to base type | - * | `Union` | `Schema.Union(A, B)` with `UndefinedKeyword`| Optional field handling | + * | `Union` | `Schema.Union([A, B])` with `UndefinedKeyword`| Optional field handling | * * ### Partial Support * * | AST Node | Schema Example | Limitation | * |-----------|-----------------------------------|-------------------------------------| - * | `Union` | `Schema.Union(A, B)` (non-optional)| Only literal unions fully supported | + * | `Union` | `Schema.Union([A, B])` (non-optional)| Only literal unions fully supported | * | `Enums` | `Schema.Enums(MyEnum)` | Not yet implemented (renders unknown)| * * ### Not Supported (renders "unknown") @@ -43,7 +43,7 @@ * | AST Node | Schema Example | Reason | * |-------------------|---------------------------|-------------------------------------| * | `BigIntKeyword` | `Schema.BigInt` | Needs bigint input handling | - * | `TupleType` | `Schema.Tuple(A, B)` | Array UI not implemented | + * | `TupleType` | `Schema.Tuple([A, B])` | Array UI not implemented | * | `TemplateLiteral` | `Schema.TemplateLiteral` | Complex string patterns | * | `UniqueSymbol` | `Schema.UniqueSymbol` | Not user-inputtable | * | `SymbolKeyword` | `Schema.Symbol` | Not user-inputtable | @@ -220,7 +220,7 @@ export const getStructProperties = ( const meta = analyzeSchema(propSchema) // Use property-level annotations if available, fall back to type annotations - // Note: annotations from Schema.optional(X).annotations({...}) are on prop, not prop.type + // Note: annotations from Schema.optional(X).annotate({...}) are on prop, not prop.type const propTitle = getTitle(prop) ?? getTitle(prop.type) const propDescription = getDescription(prop) ?? getDescription(prop.type) diff --git a/packages/@overeng/effect-schema-form/src/mod.unit.test.tsx b/packages/@overeng/effect-schema-form/src/mod.unit.test.tsx index 0bd7a48bb2..355e7e42f8 100644 --- a/packages/@overeng/effect-schema-form/src/mod.unit.test.tsx +++ b/packages/@overeng/effect-schema-form/src/mod.unit.test.tsx @@ -36,14 +36,14 @@ const propertyBytes = (properties: readonly PropertyInfo[]): string => describe('effect-schema-form baselines (cross-major invariant)', () => { const Contact = Schema.TaggedStruct('Contact', { - name: Schema.String.annotations({ + name: Schema.String.annotate({ title: 'Display name', description: 'Shown to other people', }), - age: Schema.optional(Schema.Int.annotations({ title: 'Age' })), - role: Schema.Literal('admin', 'guest'), + age: Schema.optional(Schema.Int.annotate({ title: 'Age' })), + role: Schema.Literals(['admin', 'guest']), active: Schema.Boolean, - unsupported: Schema.Tuple(Schema.String), + unsupported: Schema.Tuple([Schema.String]), }) const value: typeof Contact.Type = { @@ -57,12 +57,12 @@ describe('effect-schema-form baselines (cross-major invariant)', () => { it('encodes schema introspection metadata with the current optional and unknown partitions', () => { const cases = { annotatedString: analyzeSchema( - Schema.String.annotations({ title: 'Title', description: 'Description' }), + Schema.String.annotate({ title: 'Title', description: 'Description' }), ), optionalInt: analyzeSchema(Schema.UndefinedOr(Schema.Int)), - literalUnion: analyzeSchema(Schema.Literal('', 'kebab-case', '東京')), + literalUnion: analyzeSchema(Schema.Literals(['', 'kebab-case', '東京'])), struct: analyzeSchema(Schema.Struct({ value: Schema.String })), - tuple: analyzeSchema(Schema.Tuple(Schema.String)), + tuple: analyzeSchema(Schema.Tuple([Schema.String])), unknown: analyzeSchema(Schema.Unknown), } diff --git a/packages/@overeng/genie/bin/genie.tsx b/packages/@overeng/genie/bin/genie.tsx index 4aadad3259..a37edb0673 100644 --- a/packages/@overeng/genie/bin/genie.tsx +++ b/packages/@overeng/genie/bin/genie.tsx @@ -1,7 +1,7 @@ #!/usr/bin/env bun -import * as Cli from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import * as Cli from 'effect/unstable/cli' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect, Layer, Schema } from 'effect' import { ServiceIdentity } from '@overeng/otel-contract' @@ -31,15 +31,14 @@ const command = Cli.Command.provide(genieCommand, CurrentWorkingDirectory.live) const program = Effect.gen(function* () { const endpoint = yield* otelEndpointFromConfig() - yield* Cli.Command.run(command, { - name: 'genie', + yield* Cli.Command.runWith(command, { version, - })(rewriteHelpSubcommand(process.argv)).pipe( + })([...rewriteHelpSubcommand(process.argv).slice(2)]).pipe( Effect.scoped, CliVersion.enrichErrors, Effect.provideService(CliVersion, { name: 'genie', version }), Effect.provide( - Layer.mergeAll(NodeContext.layer, withTelemetry({ identity, shape: 'cli', endpoint })), + Layer.mergeAll(NodeServices.layer, withTelemetry({ identity, shape: 'cli', endpoint })), ), ) }) diff --git a/packages/@overeng/genie/nix/build.nix b/packages/@overeng/genie/nix/build.nix index 88e5458d3f..fb3c9418bd 100644 --- a/packages/@overeng/genie/nix/build.nix +++ b/packages/@overeng/genie/nix/build.nix @@ -26,7 +26,7 @@ let workspaceRoot = src; # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { - "." = mkSharedHash "sha256-DbFTw0rg9x++24H73gC0mh6Hio480Vzw4DcciBiFWj8="; + "." = mkSharedHash "sha256-Gg2w/VfI6tpaHDYMVk2P84NUfCaLjTkzvPBCTyNalzs="; }; nativeNodePackages = opentuiCoreNative.packages; inherit gitRev commitTs dirty; diff --git a/packages/@overeng/genie/package.json b/packages/@overeng/genie/package.json index 80352e9261..c4f80cd95d 100644 --- a/packages/@overeng/genie/package.json +++ b/packages/@overeng/genie/package.json @@ -25,17 +25,10 @@ "storybook:build": "storybook build" }, "dependencies": { - "@effect-atom/atom": "0.5.3", - "@effect-atom/atom-react": "0.5.0", - "@effect/cli": "0.75.2", - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/atom-react": "4.0.0-beta.102", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@opentui/core": "0.4.1", "@opentui/react": "0.4.1", "@overeng/otel-contract": "workspace:^", @@ -43,7 +36,7 @@ "@storybook/react": "10.4.6", "@types/react": "19.2.17", "@types/react-reconciler": "0.33.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-dom": "19.2.7", "react-reconciler": "0.33.0", @@ -51,12 +44,8 @@ "vitest": "4.1.9" }, "devDependencies": { - "@effect/cli": "0.75.2", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/printer": "0.49.0", - "@effect/printer-ansi": "0.49.0", - "@effect/vitest": "0.29.0", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@overeng/tui-core": "workspace:^", "@overeng/tui-react": "workspace:^", "@overeng/utils": "workspace:^", @@ -67,6 +56,7 @@ "@types/node": "26.0.0", "@types/react": "19.2.17", "@types/react-reconciler": "0.33.0", + "effect": "4.0.0-beta.102", "prettier": "3.8.4", "storybook": "10.4.6", "vitest": "4.1.9" @@ -77,23 +67,16 @@ } }, "peerDependencies": { - "@effect-atom/atom": "^0.5.3", - "@effect-atom/atom-react": "^0.5.0", - "@effect/cli": "^0.75.2", - "@effect/cluster": "^0.59.0", - "@effect/experimental": "^0.60.0", - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/rpc": "^0.75.1", - "@effect/workflow": "^0.18.2", + "@effect/atom-react": "^4.0.0-beta.102", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@opentui/core": "^0.4.1", "@opentui/react": "^0.4.1", "@playwright/test": "^1.61.0", "@storybook/react": "^10.4.6", "@types/react": "^19.2.17", "@types/react-reconciler": "^0.33.0", - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "react": "^19.2.7", "react-dom": "^19.2.7", "react-reconciler": "^0.33.0" diff --git a/packages/@overeng/genie/package.json.genie.ts b/packages/@overeng/genie/package.json.genie.ts index f64e710e78..ad178be800 100644 --- a/packages/@overeng/genie/package.json.genie.ts +++ b/packages/@overeng/genie/package.json.genie.ts @@ -21,28 +21,12 @@ const supportDeps = catalog.compose({ devDependencies: { workspace: [tuiCorePkg, tuiReactPkg, utilsDevPkg, utilsPkg], external: { - ...catalog.pick( - '@effect/cli', - '@effect/platform', - '@effect/platform-node', - '@effect/printer', - '@effect/printer-ansi', - '@effect/vitest', - '@types/node', - '@types/bun', - 'vitest', - '@storybook/react', - '@storybook/react-vite', - 'storybook', - '@types/react', - '@types/react-reconciler', - 'prettier', - ), + ...catalog.pick('effect', '@effect/platform-node', '@effect/vitest', '@types/node', '@types/bun', 'vitest', '@storybook/react', '@storybook/react-vite', 'storybook', '@types/react', '@types/react-reconciler', 'prettier'), }, }, peerDependencies: { workspace: [utilsPkg, tuiReactPkg], - external: catalog.pick('@effect/cli'), + external: catalog.pick('effect'), }, mode: 'install', }) diff --git a/packages/@overeng/genie/src/build/mod.integration.test.ts b/packages/@overeng/genie/src/build/mod.integration.test.ts index 910825a775..af30e89bfb 100644 --- a/packages/@overeng/genie/src/build/mod.integration.test.ts +++ b/packages/@overeng/genie/src/build/mod.integration.test.ts @@ -2,9 +2,11 @@ import * as crypto from 'node:crypto' import * as os from 'node:os' import nodePath from 'node:path' -import { Command, FileSystem, Path } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Chunk, Effect, Schema, Stream } from 'effect' +import * as FileSystem from 'effect/FileSystem' +import * as Path from 'effect/Path' +import { ChildProcess as Command } from 'effect/unstable/process' import { expect } from 'vitest' import { Vitest } from '@overeng/utils-dev/node-vitest' @@ -14,10 +16,12 @@ import { GenieApp } from './app.ts' /** Schema for parsing generated package.json in tests */ const GeneratedPackageJson = Schema.Struct({ _genieLocation: Schema.optional(Schema.String), - dependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + dependencies: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) -const decodeGeneratedPackageJson = Schema.decodeUnknownSync(Schema.parseJson(GeneratedPackageJson)) +const decodeGeneratedPackageJson = Schema.decodeUnknownSync( + Schema.fromJsonString(GeneratedPackageJson), +) type TestEnv = { root: string @@ -26,7 +30,7 @@ type TestEnv = { cleanup: () => Effect.Effect } -const TestLayer = NodeContext.layer +const TestLayer = NodeServices.layer const createTestEnv = Effect.fnUntraced(function* () { const fs = yield* FileSystem.FileSystem @@ -118,7 +122,7 @@ Vitest.describe('genie cli', () => { yield* withTestEnv((env) => Effect.gen(function* () { const packageJsonContent = yield* Schema.encode( - Schema.parseJson(Schema.Unknown, { space: 2 }), + Schema.fromJsonString(Schema.Unknown, { space: 2 }), )({ name: 'genie-cli-test', private: true }) yield* env.writeFile({ @@ -676,7 +680,7 @@ export default { data: {}, stringify: () => '{}' }`, // Flat JSON contract: final stdout line is the raw state (no envelope). // Exit code signals failure; per-file error details are carried in state. const state = yield* Schema.decodeUnknown( - Schema.parseJson(GenieApp.config.stateSchema), + Schema.fromJsonString(GenieApp.config.stateSchema), )(stdout.trim()) // Bug #135: files array must NOT be empty @@ -762,7 +766,7 @@ export default { data: {}, stringify: () => '{}' }`, // authoritative end state (no trailing Failure envelope). const lines = stdout.trim().split('\n') const finalState = yield* Schema.decodeUnknown( - Schema.parseJson(GenieApp.config.stateSchema), + Schema.fromJsonString(GenieApp.config.stateSchema), )(lines[lines.length - 1]!) expect(finalState.files.length).toBe(2) diff --git a/packages/@overeng/genie/src/build/mod.tsx b/packages/@overeng/genie/src/build/mod.tsx index fc8100f3b2..29c9debd43 100644 --- a/packages/@overeng/genie/src/build/mod.tsx +++ b/packages/@overeng/genie/src/build/mod.tsx @@ -1,8 +1,8 @@ import path from 'node:path' -import * as Cli from '@effect/cli' -import { FileSystem } from '@effect/platform' -import { Effect, Either, Fiber, Option, pipe, PubSub, Queue, Stream } from 'effect' +import { Effect, Fiber, Option, pipe, PubSub, Result, Stream } from 'effect' +import * as FileSystem from 'effect/FileSystem' +import * as Cli from 'effect/unstable/cli' import React from 'react' import { run } from '@overeng/tui-react' @@ -66,43 +66,43 @@ const dispatchEvent = (tui: { dispatch: (action: any) => void }, event: GenieEve export const genieCommand = Cli.Command.make( 'genie', { - cwd: Cli.Options.text('cwd').pipe( - Cli.Options.withDescription('Working directory to search for .genie.ts files'), - Cli.Options.withDefault('.'), + cwd: Cli.Flag.string('cwd').pipe( + Cli.Flag.withDescription('Working directory to search for .genie.ts files'), + Cli.Flag.withDefault('.'), ), - watch: Cli.Options.boolean('watch').pipe( - Cli.Options.withDescription('Watch for changes and regenerate automatically'), - Cli.Options.withDefault(false), + watch: Cli.Flag.boolean('watch').pipe( + Cli.Flag.withDescription('Watch for changes and regenerate automatically'), + Cli.Flag.withDefault(false), ), - writeable: Cli.Options.boolean('writeable').pipe( - Cli.Options.withDescription('Generate files as writable (default: read-only)'), - Cli.Options.withDefault(false), + writeable: Cli.Flag.boolean('writeable').pipe( + Cli.Flag.withDescription('Generate files as writable (default: read-only)'), + Cli.Flag.withDefault(false), ), - check: Cli.Options.boolean('check').pipe( - Cli.Options.withDescription('Check if generated files are up to date (for CI)'), - Cli.Options.withDefault(false), + check: Cli.Flag.boolean('check').pipe( + Cli.Flag.withDescription('Check if generated files are up to date (for CI)'), + Cli.Flag.withDefault(false), ), - dryRun: Cli.Options.boolean('dry-run').pipe( - Cli.Options.withDescription('Preview changes without writing files'), - Cli.Options.withDefault(false), + dryRun: Cli.Flag.boolean('dry-run').pipe( + Cli.Flag.withDescription('Preview changes without writing files'), + Cli.Flag.withDefault(false), ), - deferValidation: Cli.Options.boolean('defer-validation').pipe( - Cli.Options.withDescription( + deferValidation: Cli.Flag.boolean('defer-validation').pipe( + Cli.Flag.withDescription( 'Generate projections before a repair step; the caller must finish with genie --check', ), - Cli.Options.withDefault(false), + Cli.Flag.withDefault(false), ), - phase: Cli.Options.choice('phase', GENERATOR_PHASES).pipe( - Cli.Options.withDescription( + phase: Cli.Flag.choice('phase', GENERATOR_PHASES).pipe( + Cli.Flag.withDescription( 'Only run generators declaring this phase (bootstrap runs before install; default: all phases)', ), - Cli.Options.optional, + Cli.Flag.optional, ), - oxfmtConfig: Cli.Options.file('oxfmt-config').pipe( - Cli.Options.withDescription( + oxfmtConfig: Cli.Flag.file('oxfmt-config').pipe( + Cli.Flag.withDescription( `Path to oxfmt config file (default: ${OXFMT_CONFIG_CONVENTION_PATHS.join(' or ')})`, ), - Cli.Options.optional, + Cli.Flag.optional, ), output: outputOption, }, @@ -143,7 +143,7 @@ export const genieCommand = Cli.Command.make( // Create event bus and subscribe for TUI progress const bus = yield* PubSub.unbounded() const sub = yield* PubSub.subscribe(bus) - const consumerFiber = yield* Queue.take(sub).pipe( + const consumerFiber = yield* PubSub.take(sub).pipe( Effect.tap((event) => Effect.sync(() => dispatchEvent(tui, event))), Effect.forever, Effect.forkScoped, @@ -177,7 +177,7 @@ export const genieCommand = Cli.Command.make( yield* checkAll({ cwd: resolvedCwd, oxfmtConfigPath, phase: selectedPhase }).pipe( Effect.provideService(GenieEventBus, bus), Effect.catchTag('GenieGenerationFailedError', (error) => - syncStateFromGenerationError(error).pipe(Effect.zipRight(Effect.fail(error))), + syncStateFromGenerationError(error).pipe(Effect.andThen(Effect.fail(error))), ), ) } else { @@ -191,7 +191,7 @@ export const genieCommand = Cli.Command.make( }).pipe( Effect.provideService(GenieEventBus, bus), Effect.catchTag('GenieGenerationFailedError', (error) => - syncStateFromGenerationError(error).pipe(Effect.zipRight(Effect.fail(error))), + syncStateFromGenerationError(error).pipe(Effect.andThen(Effect.fail(error))), ), ) } @@ -232,15 +232,17 @@ export const genieCommand = Cli.Command.make( cwd: resolvedCwd, readOnly, oxfmtConfigPath, - }).pipe(Effect.either) + }).pipe(Effect.result) - if (Either.isRight(result)) { + if (Result.isSuccess(result)) { const message = - result.right._tag === 'updated' ? result.right.diffSummary : undefined + result.success._tag === 'updated' + ? result.success.diffSummary + : undefined tui.dispatch({ _tag: 'FileCompleted', path: genieFilePath, - status: mapResultToStatus(result.right), + status: mapResultToStatus(result.success), message, }) } else { @@ -248,7 +250,7 @@ export const genieCommand = Cli.Command.make( _tag: 'FileCompleted', path: genieFilePath, status: 'error', - message: result.left.message, + message: result.failure.message, }) } @@ -263,15 +265,15 @@ export const genieCommand = Cli.Command.make( } } - const watchSummary: GenieSummary = Either.isRight(result) + const watchSummary: GenieSummary = Result.isSuccess(result) ? { - created: result.right._tag === 'created' ? 1 : 0, - updated: result.right._tag === 'updated' ? 1 : 0, + created: result.success._tag === 'created' ? 1 : 0, + updated: result.success._tag === 'updated' ? 1 : 0, unchanged: newGenieFiles.length - 1 + - (result.right._tag === 'unchanged' ? 1 : 0), - skipped: result.right._tag === 'skipped' ? 1 : 0, + (result.success._tag === 'unchanged' ? 1 : 0), + skipped: result.success._tag === 'skipped' ? 1 : 0, failed: 0, } : { @@ -292,7 +294,7 @@ export const genieCommand = Cli.Command.make( Effect.ensuring( Effect.gen(function* () { const _ = yield* Fiber.interrupt(consumerFiber) - const pendingEvents = yield* Queue.takeAll(sub) + const pendingEvents = yield* PubSub.takeUpTo(sub, Number.POSITIVE_INFINITY) for (const event of pendingEvents) { yield* Effect.sync(() => dispatchEvent(tui, event)) } diff --git a/packages/@overeng/genie/src/build/types.ts b/packages/@overeng/genie/src/build/types.ts index 66d9f5a843..ccc9a01700 100644 --- a/packages/@overeng/genie/src/build/types.ts +++ b/packages/@overeng/genie/src/build/types.ts @@ -1,6 +1,8 @@ -import type { Error as PlatformError, FileSystem, Path } from '@effect/platform' -import type * as CommandExecutor from '@effect/platform/CommandExecutor' +import type { Error as PlatformError } from 'effect' +import type { FileSystem } from 'effect/FileSystem' +import type { Path } from 'effect/Path' import type { Option } from 'effect' +import type { ChildProcessSpawner } from 'effect/unstable/process' import type { OutputModeValue } from '@overeng/tui-react/node' import type { CurrentWorkingDirectory } from '@overeng/utils/node' @@ -26,7 +28,7 @@ export type GenieCommandConfig = { export type GenieCommandEnv = | FileSystem.FileSystem | Path.Path - | CommandExecutor.CommandExecutor + | ChildProcessSpawner.ChildProcessSpawner | CurrentWorkingDirectory /** Possible errors from genie command execution */ diff --git a/packages/@overeng/genie/src/build/view.tsx b/packages/@overeng/genie/src/build/view.tsx index 9580bbbd6c..0ce4be5c8e 100644 --- a/packages/@overeng/genie/src/build/view.tsx +++ b/packages/@overeng/genie/src/build/view.tsx @@ -5,7 +5,7 @@ * Handles both progress display (TTY) and final output (all modes). */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React, { useMemo } from 'react' import { diff --git a/packages/@overeng/genie/src/core/core.ts b/packages/@overeng/genie/src/core/core.ts index 1fc46a5872..b19d826106 100644 --- a/packages/@overeng/genie/src/core/core.ts +++ b/packages/@overeng/genie/src/core/core.ts @@ -1,10 +1,11 @@ import os from 'node:os' import path from 'node:path' -import { type Error as PlatformError, FileSystem } from '@effect/platform' -import type * as CommandExecutor from '@effect/platform/CommandExecutor' -import type { Path } from '@effect/platform/Path' -import { Effect, Either, Option, Ref } from 'effect' +import type { Error as PlatformError } from 'effect' +import * as FileSystem from 'effect/FileSystem' +import { Effect, Result, Option, Ref } from 'effect' +import type { Path } from 'effect/Path' +import type { ChildProcessSpawner } from 'effect/unstable/process' import { assertNever } from '@overeng/utils' @@ -174,9 +175,9 @@ const runValidationOrFail = Effect.fn('genie/runValidationOrFail')(function* ({ cwd, ...(genieFiles !== undefined ? { genieFiles } : {}), ...(preloadedFiles !== undefined ? { preloadedFiles } : {}), - }).pipe(Effect.either) - if (Either.isLeft(validationResult) === true) { - const error = validationResult.left + }).pipe(Effect.result) + if (Result.isFailure(validationResult) === true) { + const error = validationResult.failure const message = error instanceof Error ? error.message : String(error) yield* emit({ _tag: 'Error', message }) return yield* new GenieGenerationFailedError({ @@ -186,7 +187,7 @@ const runValidationOrFail = Effect.fn('genie/runValidationOrFail')(function* ({ }) } - const warnings = validationResult.right + const warnings = validationResult.success if (warnings.length > 0) { const formatted = formatValidationIssues(warnings) yield* emit({ _tag: 'ValidationWarnings', message: formatted }) @@ -204,7 +205,7 @@ export const generateAll = ({ }: CoreGenerateOptions): Effect.Effect< GenieGenerateResult, GenieGenerationFailedError | PlatformError.PlatformError, - FileSystem.FileSystem | Path | CommandExecutor.CommandExecutor | GenieEventBus + FileSystem.FileSystem | Path | ChildProcessSpawner.ChildProcessSpawner | GenieEventBus > => Effect.gen(function* () { const genieFiles = yield* discoverAndValidate(cwd, phase) @@ -235,16 +236,16 @@ export const generateAll = ({ readOnly, dryRun, oxfmtConfigPath, - }).pipe(Effect.either) + }).pipe(Effect.result) - if (Either.isRight(result) === true) { - const status = mapResultToStatus(result.right) + if (Result.isSuccess(result) === true) { + const status = mapResultToStatus(result.success) yield* emit({ _tag: 'FileCompleted', path: genieFilePath, status, - ...(result.right._tag === 'updated' && result.right.diffSummary !== undefined - ? { message: result.right.diffSummary } + ...(result.success._tag === 'updated' && result.success.diffSummary !== undefined + ? { message: result.success.diffSummary } : {}), }) } else { @@ -252,7 +253,7 @@ export const generateAll = ({ _tag: 'FileCompleted', path: genieFilePath, status: 'error', - message: result.left.message, + message: result.failure.message, }) } @@ -262,8 +263,8 @@ export const generateAll = ({ { concurrency: 'unbounded' }, ) - const successes = results.filter(Either.isRight).map((r) => r.right) - const failures = results.filter(Either.isLeft).map((r) => r.left) + const successes = results.filter(Result.isSuccess).map((r) => r.success) + const failures = results.filter(Result.isFailure).map((r) => r.failure) // Surface CatalogConflictError from initial failures before TDZ re-validation const catalogConflict = failures @@ -281,13 +282,13 @@ export const generateAll = ({ }> = [] for (const genieFilePath of genieFiles) { - const result = yield* checkFile({ genieFilePath, cwd, oxfmtConfigPath }).pipe(Effect.either) + const result = yield* checkFile({ genieFilePath, cwd, oxfmtConfigPath }).pipe(Effect.result) - if (Either.isLeft(result) === true) { + if (Result.isFailure(result) === true) { revalidateErrors.push({ genieFilePath, - error: result.left, - isRootCause: errorOriginatesInFile({ error: result.left, filePath: genieFilePath }), + error: result.failure, + isRootCause: errorOriginatesInFile({ error: result.failure, filePath: genieFilePath }), }) } } @@ -341,18 +342,18 @@ export const generateAll = ({ message: `${summary.failed} file(s) failed to generate`, files: genieFiles.map((p, i) => { const resultEither = results[i]! - if (Either.isRight(resultEither) === true) { + if (Result.isSuccess(resultEither) === true) { return { path: p, relativePath: path.relative(cwd, p.replace('.genie.ts', '')), - status: mapResultToStatus(resultEither.right), + status: mapResultToStatus(resultEither.success), } } return { path: p, relativePath: path.relative(cwd, p.replace('.genie.ts', '')), status: 'error' as GenieFileStatus, - message: resultEither.left.message, + message: resultEither.failure.message, } }), }) @@ -382,7 +383,7 @@ export const checkAll = ({ }: CoreCheckOptions): Effect.Effect< void, GenieGenerationFailedError | PlatformError.PlatformError, - FileSystem.FileSystem | Path | CommandExecutor.CommandExecutor | GenieEventBus + FileSystem.FileSystem | Path | ChildProcessSpawner.ChildProcessSpawner | GenieEventBus > => Effect.gen(function* () { const checkConcurrency = Math.max( @@ -474,16 +475,16 @@ export const checkAll = ({ yield* emit({ _tag: 'FileStarted', path: genieFilePath }) const result = yield* checkFileDetailed({ genieFilePath, cwd, oxfmtConfigPath }).pipe( - Effect.either, + Effect.result, ) - if (Either.isRight(result) === true) { + if (Result.isSuccess(result) === true) { yield* completeFile({ path: genieFilePath, result: { _tag: 'success', path: genieFilePath, - loadedGenieFile: result.right.loadedGenieFile, + loadedGenieFile: result.success.loadedGenieFile, }, }) return @@ -492,24 +493,24 @@ export const checkAll = ({ const errorResult: FileCheckResult = { _tag: 'error', path: genieFilePath, - message: result.left.message, + message: result.failure.message, } yield* completeFile({ path: genieFilePath, result: errorResult }) - if (result.left._tag === 'GenieCheckError') { + if (result.failure._tag === 'GenieCheckError') { return } return yield* Effect.fail({ _tag: 'FatalCheckFailure', path: genieFilePath, - message: result.left.message, + message: result.failure.message, }) }), { concurrency: checkConcurrency }, - ).pipe(Effect.either) + ).pipe(Effect.result) - if (Either.isLeft(checkResult) === true) { + if (Result.isFailure(checkResult) === true) { const completedPaths = yield* Ref.get(completedPathsRef) const interruptedPaths = genieFiles.filter((p) => !completedPaths.has(p)) @@ -541,8 +542,8 @@ export const checkAll = ({ failedCount: failed, message: interruptedPaths.length > 0 - ? `Fatal check error in ${path.relative(cwd, checkResult.left.path)}; interrupted ${interruptedPaths.length} sibling file(s)` - : `Fatal check error in ${path.relative(cwd, checkResult.left.path)}`, + ? `Fatal check error in ${path.relative(cwd, checkResult.failure.path)}; interrupted ${interruptedPaths.length} sibling file(s)` + : `Fatal check error in ${path.relative(cwd, checkResult.failure.path)}`, files: genieFiles.map((p) => { const r = allResults.get(p) return { diff --git a/packages/@overeng/genie/src/core/discovery.ts b/packages/@overeng/genie/src/core/discovery.ts index 872495f79f..6dc1d94af0 100644 --- a/packages/@overeng/genie/src/core/discovery.ts +++ b/packages/@overeng/genie/src/core/discovery.ts @@ -2,7 +2,9 @@ import { execFileSync } from 'node:child_process' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { type Error as PlatformError, FileSystem, Path } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import * as FileSystem from 'effect/FileSystem' +import * as Path from 'effect/Path' import { Effect, Option } from 'effect' import { resolveImportMapSpecifierForImporterSync } from './import-map/mod.ts' diff --git a/packages/@overeng/genie/src/core/discovery.unit.test.ts b/packages/@overeng/genie/src/core/discovery.unit.test.ts index 59226a7d8b..12d6a2e3df 100644 --- a/packages/@overeng/genie/src/core/discovery.unit.test.ts +++ b/packages/@overeng/genie/src/core/discovery.unit.test.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs/promises' import * as os from 'node:os' import path from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect } from 'effect' import { describe, expect, it } from 'vitest' @@ -54,7 +54,7 @@ describe('findGenieFiles', () => { }) const discovered = await Effect.runPromise( - findGenieFiles(root).pipe(Effect.provide(NodeContext.layer)), + findGenieFiles(root).pipe(Effect.provide(NodeServices.layer)), ) const relative = await toCanonicalRelative({ root, files: discovered }) @@ -102,7 +102,7 @@ describe('findGenieFiles', () => { ) const discovered = await Effect.runPromise( - findGenieFiles(root).pipe(Effect.provide(NodeContext.layer)), + findGenieFiles(root).pipe(Effect.provide(NodeServices.layer)), ) const relative = await toCanonicalRelative({ root, files: discovered }) @@ -125,7 +125,7 @@ describe('findGenieFiles', () => { }) const discovered = await Effect.runPromise( - findGenieFiles(symlinkRoot).pipe(Effect.provide(NodeContext.layer)), + findGenieFiles(symlinkRoot).pipe(Effect.provide(NodeServices.layer)), ) const relative = await toCanonicalRelative({ root: symlinkRoot, files: discovered }) diff --git a/packages/@overeng/genie/src/core/error-detection.unit.test.ts b/packages/@overeng/genie/src/core/error-detection.unit.test.ts index 573056aac2..93d4fc13a7 100644 --- a/packages/@overeng/genie/src/core/error-detection.unit.test.ts +++ b/packages/@overeng/genie/src/core/error-detection.unit.test.ts @@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises' import * as os from 'node:os' import path from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Duration, Effect, Either, Option, PubSub } from 'effect' import { describe, expect, it } from 'vitest' @@ -166,7 +166,7 @@ export default { oxfmtConfigPath: Option.none(), }).pipe( Effect.provideService(GenieEventBus, bus), - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.timeout(Duration.seconds(2)), Effect.either, ) diff --git a/packages/@overeng/genie/src/core/errors.ts b/packages/@overeng/genie/src/core/errors.ts index 0f11191941..c04837d8e1 100644 --- a/packages/@overeng/genie/src/core/errors.ts +++ b/packages/@overeng/genie/src/core/errors.ts @@ -12,21 +12,24 @@ import { GenieFile } from './schema.ts' * * @see {@link ./generation.ts#isTdzError} for TDZ detection logic */ -export class GenieImportError extends Schema.TaggedError()('GenieImportError', { - genieFilePath: Schema.String, - message: Schema.String, - /** The original error - preserved for TDZ detection and root cause analysis */ - cause: Schema.Defect, -}) {} +export class GenieImportError extends Schema.TaggedErrorClass()( + 'GenieImportError', + { + genieFilePath: Schema.String, + message: Schema.String, + /** The original error - preserved for TDZ detection and root cause analysis */ + cause: Schema.Defect(), + }, +) {} /** Error when generated file content doesn't match (in check mode) */ -export class GenieCheckError extends Schema.TaggedError()('GenieCheckError', { +export class GenieCheckError extends Schema.TaggedErrorClass()('GenieCheckError', { targetFilePath: Schema.String, message: Schema.String, }) {} /** Error when one or more files failed to generate */ -export class GenieGenerationFailedError extends Schema.TaggedError()( +export class GenieGenerationFailedError extends Schema.TaggedErrorClass()( 'GenieGenerationFailedError', { failedCount: Schema.Number, @@ -47,15 +50,15 @@ export class GenieGenerationFailedError extends Schema.TaggedError()('GenieFileError', { +export class GenieFileError extends Schema.TaggedErrorClass()('GenieFileError', { targetFilePath: Schema.String, message: Schema.String, /** The original error - preserved for TDZ detection and root cause analysis */ - cause: Schema.Defect, + cause: Schema.Defect(), }) {} /** Error when genie validation fails */ -export class GenieValidationError extends Schema.TaggedError()( +export class GenieValidationError extends Schema.TaggedErrorClass()( 'GenieValidationError', { message: Schema.String, @@ -63,7 +66,7 @@ export class GenieValidationError extends Schema.TaggedError()( +export class GenieNotImplementedError extends Schema.TaggedErrorClass()( 'GenieNotImplementedError', { message: Schema.String, @@ -71,7 +74,7 @@ export class GenieNotImplementedError extends Schema.TaggedError()( +export class InvalidOxfmtConfigError extends Schema.TaggedErrorClass()( 'InvalidOxfmtConfigError', { message: Schema.String, diff --git a/packages/@overeng/genie/src/core/events.ts b/packages/@overeng/genie/src/core/events.ts index 71f0509504..9ca4255bac 100644 --- a/packages/@overeng/genie/src/core/events.ts +++ b/packages/@overeng/genie/src/core/events.ts @@ -3,7 +3,7 @@ import { Context, Effect, PubSub, Schema } from 'effect' import { GenieFileStatus, GenieSummary } from './schema.ts' /** Schema for events emitted during genie generation/check. */ -export const GenieEvent = Schema.Union( +export const GenieEvent = Schema.Union([ Schema.TaggedStruct('FilesDiscovered', { files: Schema.Array( Schema.Struct({ @@ -26,15 +26,14 @@ export const GenieEvent = Schema.Union( Schema.TaggedStruct('Error', { message: Schema.String }), Schema.TaggedStruct('ValidationWarnings', { message: Schema.String }), -) +]) /** Inferred union of all progress events emitted during generation/check. */ export type GenieEvent = typeof GenieEvent.Type /** PubSub for genie progress events, injected via Context. */ -export class GenieEventBus extends Context.Tag('@overeng/genie/EventBus')< - GenieEventBus, - PubSub.PubSub ->() {} +export class GenieEventBus extends Context.Service>()( + '@overeng/genie/EventBus', +) {} /** Publish a GenieEvent to the bus from Context. */ export const emit = (event: GenieEvent): Effect.Effect => diff --git a/packages/@overeng/genie/src/core/generation.ts b/packages/@overeng/genie/src/core/generation.ts index e052e74f71..71767eb7e5 100644 --- a/packages/@overeng/genie/src/core/generation.ts +++ b/packages/@overeng/genie/src/core/generation.ts @@ -5,14 +5,12 @@ import os from 'node:os' import path from 'node:path' import { pathToFileURL } from 'node:url' -import type { Path } from '@effect/platform' -import { - Command, - type CommandExecutor, - type Error as PlatformError, - FileSystem, -} from '@effect/platform' -import { Duration, Effect, Either, Option, Schema } from 'effect' +import type { Path } from 'effect/Path' +import type { PlatformError } from 'effect' +import * as FileSystem from 'effect/FileSystem' +import { Duration, Effect, Option, Result, Schema, Stream } from 'effect' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import type { ChildProcessSpawner } from 'effect/unstable/process' import { DistributedSemaphore } from '@overeng/utils/lock' import { FileSystemBacking } from '@overeng/utils/node' @@ -315,7 +313,7 @@ const oxfmtSupportedExtensions = new Set(['.json', '.jsonc', '.yml', '.yaml']) type OxfmtConfig = Readonly> /** Permissive JSON decode for an oxfmt config (shape is cast, not validated). */ -const decodeOxfmtConfig = Schema.decodeUnknownSync(Schema.parseJson(Schema.Unknown)) +const decodeOxfmtConfig = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)) const loadOxfmtConfig = Effect.fn('loadOxfmtConfig')(function* ({ configPath, @@ -456,8 +454,8 @@ const formatWithOxfmt = Effect.fn('formatWithOxfmt')(function* ({ return content } - const optionsResult = yield* loadOxfmtConfig({ configPath }).pipe(Effect.either) - if (Either.isLeft(optionsResult) === true) { + const optionsResult = yield* loadOxfmtConfig({ configPath }).pipe(Effect.result) + if (Result.isFailure(optionsResult) === true) { return content } @@ -466,11 +464,14 @@ const formatWithOxfmt = Effect.fn('formatWithOxfmt')(function* ({ onSome: (cfg) => ['-c', cfg, '--stdin-filepath', targetFilePath], }) - const result = yield* Command.make('oxfmt', ...args).pipe( - Command.feed(content), - Command.string, - Effect.orElseSucceed(() => content), - ) + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const result = yield* spawner + .string( + Command.make('oxfmt', args, { + stdin: Stream.make(new TextEncoder().encode(content)), + }), + ) + .pipe(Effect.orElseSucceed(() => content)) // If oxfmt returned empty output (e.g., failed to parse), return original content. // This handles YAML with GitHub Actions ${{ }} expressions in flow sequences (inline arrays) @@ -708,7 +709,7 @@ const atomicWriteFile = ({ // Make target writable if it exists (for read-only files) const targetExists = yield* fs.exists(targetFilePath) if (targetExists === true) { - yield* fs.chmod(targetFilePath, 0o644).pipe(Effect.catchAll(() => Effect.void)) + yield* fs.chmod(targetFilePath, 0o644).pipe(Effect.catch(() => Effect.void)) } // Write to temp file first @@ -722,12 +723,12 @@ const atomicWriteFile = ({ // Atomic rename - either fully succeeds or original file remains untouched yield* fs.rename(tempPath, targetFilePath) }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { // Clean up temp file on failure const fs = yield* FileSystem.FileSystem const tempPath = `${targetFilePath}.genie.tmp` - yield* fs.remove(tempPath, { force: true }).pipe(Effect.catchAll(() => Effect.void)) + yield* fs.remove(tempPath, { force: true }).pipe(Effect.catch(() => Effect.void)) return yield* error }), ), @@ -776,7 +777,7 @@ export const generateFile = ({ }): Effect.Effect< GenerateSuccess, GenieFileError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor | Path.Path + FileSystem.FileSystem | ChildProcessSpawner.ChildProcessSpawner | Path.Path > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem @@ -824,7 +825,7 @@ export const generateFile = ({ // Restore read-only permissions if needed (e.g. after a --writeable run or manual chmod) const mode = generatedFileMode({ readOnly, targetFilePath }) if (mode !== undefined) { - yield* fs.chmod(targetFilePath, mode).pipe(Effect.catchAll(() => Effect.void)) + yield* fs.chmod(targetFilePath, mode).pipe(Effect.catch(() => Effect.void)) } return { _tag: 'unchanged', targetFilePath } as const } @@ -860,7 +861,7 @@ export const generateFile = ({ underlyingError instanceof Error ? underlyingError : new Error(safeErrorString(cause)), }) }), - Effect.catchAllDefect((defect) => { + Effect.catchDefect((defect) => { const targetFilePath = genieFilePath.replace('.genie.ts', '') return Effect.fail( new GenieFileError({ @@ -891,7 +892,7 @@ export const checkFile = ({ }): Effect.Effect< void, GenieCheckError | GenieImportError | PlatformError.PlatformError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor + FileSystem.FileSystem | ChildProcessSpawner.ChildProcessSpawner > => checkFileDetailed({ genieFilePath, cwd, oxfmtConfigPath }).pipe(Effect.asVoid) /** Check a generated file and return the loaded genie module for downstream validation reuse. */ @@ -906,7 +907,7 @@ export const checkFileDetailed = ({ }): Effect.Effect< { targetFilePath: string; loadedGenieFile: LoadedGenieFile }, GenieCheckError | GenieImportError | PlatformError.PlatformError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor + FileSystem.FileSystem | ChildProcessSpawner.ChildProcessSpawner > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem diff --git a/packages/@overeng/genie/src/core/import-map/import-map.unit.test.ts b/packages/@overeng/genie/src/core/import-map/import-map.unit.test.ts index cdc23f81ef..bab354b01f 100644 --- a/packages/@overeng/genie/src/core/import-map/import-map.unit.test.ts +++ b/packages/@overeng/genie/src/core/import-map/import-map.unit.test.ts @@ -2,8 +2,8 @@ import * as os from 'node:os' import * as path from 'node:path' import { pathToFileURL } from 'node:url' -import { FileSystem } from '@effect/platform' import { NodeFileSystem } from '@effect/platform-node' +import * as FileSystem from 'effect/FileSystem' import { Effect, Option, Schema } from 'effect' import { afterEach, beforeEach, expect, it } from 'vitest' @@ -26,7 +26,7 @@ const GENIE_MEMBER_SOURCE_MAP_ENV = 'GENIE_MEMBER_SOURCE_MAP' const MEGAREPO_STORE_ENV = 'MEGAREPO_STORE' /** Type-safe JSON stringify using Schema */ -const toJson = Schema.encodeSync(Schema.parseJson(Schema.Unknown)) +const toJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)) /** Create a temp directory for each test */ const makeTempDir = Effect.gen(function* () { @@ -46,7 +46,7 @@ const removeTempDir = Effect.fnUntraced( const fs = yield* FileSystem.FileSystem yield* fs.remove(tempDir, { recursive: true }) }, - Effect.catchAll(() => Effect.void), + Effect.catch(() => Effect.void), ) /** Write a file with content */ diff --git a/packages/@overeng/genie/src/core/import-map/mod.ts b/packages/@overeng/genie/src/core/import-map/mod.ts index c3f9d50458..805f1c6cb5 100644 --- a/packages/@overeng/genie/src/core/import-map/mod.ts +++ b/packages/@overeng/genie/src/core/import-map/mod.ts @@ -14,8 +14,8 @@ import * as path from 'node:path' import { pathToFileURL } from 'node:url' -import { FileSystem } from '@effect/platform' -import { Effect, Option } from 'effect' +import * as FileSystem from 'effect/FileSystem' +import { Effect, Option, Result } from 'effect' import * as Observability from '../observability.ts' import { @@ -84,12 +84,9 @@ export const findPackageJsonWithImports = Effect.fn('findPackageJsonWithImports' .exists(packageJsonPath) .pipe(Effect.orElseSucceed(() => false)) if (pkgExists === true) { - const contentResult = yield* effectFs.readFileString(packageJsonPath).pipe( - Effect.either, - Effect.orElseSucceed(() => ({ _tag: 'Left' as const, left: null })), - ) - if (contentResult._tag === 'Right') { - const importMap = parseImportMapFromPackageJsonContent(contentResult.right) + const contentResult = yield* effectFs.readFileString(packageJsonPath).pipe(Effect.result) + if (Result.isSuccess(contentResult) === true) { + const importMap = parseImportMapFromPackageJsonContent(contentResult.success) if (Object.keys(importMap).length > 0) { return Option.some(packageJsonPath) } @@ -102,12 +99,9 @@ export const findPackageJsonWithImports = Effect.fn('findPackageJsonWithImports' .exists(genieSourcePath) .pipe(Effect.orElseSucceed(() => false)) if (genieExists === true) { - const sourceResult = yield* effectFs.readFileString(genieSourcePath).pipe( - Effect.either, - Effect.orElseSucceed(() => ({ _tag: 'Left' as const, left: null })), - ) - if (sourceResult._tag === 'Right') { - const importMap = parseImportMapFromGenieSource(sourceResult.right) + const sourceResult = yield* effectFs.readFileString(genieSourcePath).pipe(Effect.result) + if (Result.isSuccess(sourceResult) === true) { + const importMap = parseImportMapFromGenieSource(sourceResult.success) if (Object.keys(importMap).length > 0) { return Option.some(packageJsonPath) } @@ -135,12 +129,9 @@ export const extractImportMap = Effect.fn('extractImportMap')(function* (package // First try the generated package.json const pkgExists = yield* effectFs.exists(packageJsonPath).pipe(Effect.orElseSucceed(() => false)) if (pkgExists === true) { - const contentResult = yield* effectFs.readFileString(packageJsonPath).pipe( - Effect.either, - Effect.orElseSucceed(() => ({ _tag: 'Left' as const, left: null })), - ) - if (contentResult._tag === 'Right') { - const importMap = parseImportMapFromPackageJsonContent(contentResult.right) + const contentResult = yield* effectFs.readFileString(packageJsonPath).pipe(Effect.result) + if (Result.isSuccess(contentResult) === true) { + const importMap = parseImportMapFromPackageJsonContent(contentResult.success) if (Object.keys(importMap).length > 0) { return importMap } @@ -154,12 +145,9 @@ export const extractImportMap = Effect.fn('extractImportMap')(function* (package .exists(genieSourcePath) .pipe(Effect.orElseSucceed(() => false)) if (genieExists === true) { - const sourceResult = yield* effectFs.readFileString(genieSourcePath).pipe( - Effect.either, - Effect.orElseSucceed(() => ({ _tag: 'Left' as const, left: null })), - ) - if (sourceResult._tag === 'Right') { - const importMap = parseImportMapFromGenieSource(sourceResult.right) + const sourceResult = yield* effectFs.readFileString(genieSourcePath).pipe(Effect.result) + if (Result.isSuccess(sourceResult) === true) { + const importMap = parseImportMapFromGenieSource(sourceResult.success) if (Object.keys(importMap).length > 0) { return importMap } diff --git a/packages/@overeng/genie/src/core/observability.ts b/packages/@overeng/genie/src/core/observability.ts index b805b67e28..eabb5cff12 100644 --- a/packages/@overeng/genie/src/core/observability.ts +++ b/packages/@overeng/genie/src/core/observability.ts @@ -35,7 +35,7 @@ const trustOtelContract = ( effect: Effect.Effect, ): Effect.Effect => effect.pipe( - Effect.catchAll((error) => + Effect.catch((error) => typeof error === 'object' && error !== null && '_tag' in error && diff --git a/packages/@overeng/genie/src/core/package-json-context.ts b/packages/@overeng/genie/src/core/package-json-context.ts index a0e4f15e94..b1a5f330d5 100644 --- a/packages/@overeng/genie/src/core/package-json-context.ts +++ b/packages/@overeng/genie/src/core/package-json-context.ts @@ -1,4 +1,5 @@ -import { FileSystem, Path } from '@effect/platform' +import * as FileSystem from 'effect/FileSystem' +import * as Path from 'effect/Path' import { Effect, Schema } from 'effect' import type { PackageInfo } from '../common/types.ts' @@ -18,7 +19,7 @@ export type WorkspaceProvider = { const normalizePath = (input: string): string => input.replace(/\\/g, '/') /** Permissive JSON decode for package.json files (shape is cast, not validated). */ -const decodePackageJson = Schema.decodeUnknownSync(Schema.parseJson(Schema.Unknown)) +const decodePackageJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)) /** Build validation context by reading all workspace package.json files into a lookup map */ export const buildPackageJsonValidationContext = Effect.fn( @@ -33,7 +34,7 @@ export const buildPackageJsonValidationContext = Effect.fn( for (const packageJsonPath of packageJsonPaths) { const content = yield* fs .readFileString(packageJsonPath) - .pipe(Effect.catchAll(() => Effect.void)) + .pipe(Effect.catch(() => Effect.void)) if (content === undefined) continue const parsed = Effect.try({ try: () => decodePackageJson(content) as Omit, diff --git a/packages/@overeng/genie/src/core/schema.ts b/packages/@overeng/genie/src/core/schema.ts index 52d6f14f38..f36b40a41c 100644 --- a/packages/@overeng/genie/src/core/schema.ts +++ b/packages/@overeng/genie/src/core/schema.ts @@ -12,7 +12,7 @@ import { Schema } from 'effect' // ============================================================================= /** Schema for file processing status values */ -export const GenieFileStatus = Schema.Literal( +export const GenieFileStatus = Schema.Literals([ 'pending', 'active', 'created', @@ -20,7 +20,7 @@ export const GenieFileStatus = Schema.Literal( 'unchanged', 'skipped', 'error', -) +]) /** File processing status */ export type GenieFileStatus = Schema.Schema.Type @@ -66,7 +66,7 @@ export type GenieSummary = Schema.Schema.Type // ============================================================================= /** Schema for genie operation phases */ -export const GeniePhase = Schema.Literal('discovering', 'generating', 'complete', 'error') +export const GeniePhase = Schema.Literals(['discovering', 'generating', 'complete', 'error']) /** Genie operation phase */ export type GeniePhase = Schema.Schema.Type @@ -75,7 +75,7 @@ export type GeniePhase = Schema.Schema.Type // ============================================================================= /** Schema for genie operation modes */ -export const GenieMode = Schema.Literal('generate', 'check', 'dry-run') +export const GenieMode = Schema.Literals(['generate', 'check', 'dry-run']) /** Genie operation mode */ export type GenieMode = Schema.Schema.Type @@ -123,7 +123,7 @@ export type GenieState = Schema.Schema.Type // ============================================================================= /** Schema for genie state actions */ -export const GenieAction = Schema.Union( +export const GenieAction = Schema.Union([ /** Replace entire state */ Schema.TaggedStruct('SetState', { state: GenieState }), @@ -160,7 +160,7 @@ export const GenieAction = Schema.Union( /** Watch mode - reset for new cycle */ Schema.TaggedStruct('WatchReset', {}), -) +]) /** Genie state action type */ export type GenieAction = Schema.Schema.Type diff --git a/packages/@overeng/genie/src/core/types.ts b/packages/@overeng/genie/src/core/types.ts index e5e6fa83e6..e4a68ae038 100644 --- a/packages/@overeng/genie/src/core/types.ts +++ b/packages/@overeng/genie/src/core/types.ts @@ -4,7 +4,7 @@ import { Schema } from 'effect' export type { GenieContext } from '../runtime/mod.ts' /** Schema for the result of generating a single file */ -export const GenerateSuccess = Schema.Union( +export const GenerateSuccess = Schema.Union([ Schema.TaggedStruct('created', { targetFilePath: Schema.String }), Schema.TaggedStruct('updated', { targetFilePath: Schema.String, @@ -12,13 +12,13 @@ export const GenerateSuccess = Schema.Union( }), Schema.TaggedStruct('unchanged', { targetFilePath: Schema.String }), Schema.TaggedStruct('skipped', { targetFilePath: Schema.String, reason: Schema.String }), -) +]) export type GenerateSuccess = typeof GenerateSuccess.Type /** Schema for the result of attempting to stat a file - handles broken symlinks gracefully */ -export const StatResult = Schema.Union( +export const StatResult = Schema.Union([ Schema.Struct({ type: Schema.Literal('directory') }), Schema.Struct({ type: Schema.Literal('file') }), Schema.Struct({ type: Schema.Literal('skip'), reason: Schema.String }), -) +]) export type StatResult = typeof StatResult.Type diff --git a/packages/@overeng/genie/src/core/validation.ts b/packages/@overeng/genie/src/core/validation.ts index 9529d51afe..506ae17c4f 100644 --- a/packages/@overeng/genie/src/core/validation.ts +++ b/packages/@overeng/genie/src/core/validation.ts @@ -1,4 +1,6 @@ -import { type Error as PlatformError, FileSystem, Path } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import * as FileSystem from 'effect/FileSystem' +import * as Path from 'effect/Path' import { Effect } from 'effect' import ts from 'typescript' @@ -71,7 +73,7 @@ export const runGenieValidation = ({ } return loadGenieFile({ genieFilePath, cwd }) })().pipe( - Effect.catchAll((error) => { + Effect.catch((error) => { issues.push({ severity: 'error', packageName: 'genie', diff --git a/packages/@overeng/genie/src/core/workspace.ts b/packages/@overeng/genie/src/core/workspace.ts index 48e808567b..d42b17f111 100644 --- a/packages/@overeng/genie/src/core/workspace.ts +++ b/packages/@overeng/genie/src/core/workspace.ts @@ -1,4 +1,5 @@ -import { FileSystem, Path } from '@effect/platform' +import * as FileSystem from 'effect/FileSystem' +import * as Path from 'effect/Path' import { Effect } from 'effect' import { matchesAnyPattern } from '../runtime/package-json/validation.ts' @@ -34,7 +35,7 @@ const findWorkspaceRoot = Effect.fn('workspace/findWorkspaceRoot')(function* ({ let currentDir = cwd while (true) { const workspaceFile = pathService.join(currentDir, 'pnpm-workspace.yaml') - const stat = yield* fs.stat(workspaceFile).pipe(Effect.catchAll(() => Effect.void)) + const stat = yield* fs.stat(workspaceFile).pipe(Effect.catch(() => Effect.void)) if (stat?.type === 'File') { return currentDir } @@ -64,7 +65,7 @@ const findPackageJsonDirs = Effect.fn('workspace/findPackageJsonDirs')(function* for (const entry of entries) { if (shouldSkipDir(entry) === true) continue const fullPath = pathService.join(dir, entry) - const stat = yield* fs.stat(fullPath).pipe(Effect.catchAll(() => Effect.void)) + const stat = yield* fs.stat(fullPath).pipe(Effect.catch(() => Effect.void)) if (stat === undefined) continue if (stat.type === 'Directory') { yield* walk(fullPath) diff --git a/packages/@overeng/genie/src/runtime/composition/mod.ts b/packages/@overeng/genie/src/runtime/composition/mod.ts index 723936135f..6069a17f92 100644 --- a/packages/@overeng/genie/src/runtime/composition/mod.ts +++ b/packages/@overeng/genie/src/runtime/composition/mod.ts @@ -120,7 +120,7 @@ export const tsconfigReferencesFromPackages = ({ // `tsconfigReferencesFromPackages` above: read structured `meta` off member outputs, dedupe, // deterministically sort. It NEVER touches emitted files. Whole-registry integrity // (namespace uniqueness + global ref resolution) is surfaced as genie validation issues -// (this layer is dep-free — Effect/`Schema.TaggedError` are not available here; per-member +// (this layer is dep-free — Effect/`Schema.TaggedErrorClass` are not available here; per-member // author-time errors are raised as tagged errors in `@overeng/otel-contract` `./registry`). // --------------------------------------------------------------------------- diff --git a/packages/@overeng/genie/src/runtime/semver/mod.ts b/packages/@overeng/genie/src/runtime/semver/mod.ts index b9e1664995..130225b024 100644 --- a/packages/@overeng/genie/src/runtime/semver/mod.ts +++ b/packages/@overeng/genie/src/runtime/semver/mod.ts @@ -10,7 +10,7 @@ export type SemVer = readonly [major: number, minor: number, patch: number] /** Parse a version string like `1.2.3` into a `[major, minor, patch]` tuple. */ export const parseVersion = (version: string): SemVer => { - const cleaned = version.replace(/\.x/g, '.0') + const cleaned = version.replace(/\.x/g, '.0').split('-')[0] ?? version const parts = cleaned.split('.').map(Number) return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0] as const } diff --git a/packages/@overeng/genie/src/runtime/semver/semver.unit.test.ts b/packages/@overeng/genie/src/runtime/semver/semver.unit.test.ts index 016ac475a9..fdb9fc1a24 100644 --- a/packages/@overeng/genie/src/runtime/semver/semver.unit.test.ts +++ b/packages/@overeng/genie/src/runtime/semver/semver.unit.test.ts @@ -16,6 +16,10 @@ describe('parseVersion', () => { it('handles two-part versions', () => { expect(parseVersion('3.19')).toEqual([3, 19, 0]) }) + + it('ignores prerelease suffixes for catalog peer range validation', () => { + expect(parseVersion('4.0.0-beta.102')).toEqual([4, 0, 0]) + }) }) describe('satisfiesRange', () => { @@ -43,6 +47,7 @@ describe('satisfiesRange', () => { expect(satisfiesRange('3.21.0', '^3.19.15')).toBe(true) expect(satisfiesRange('3.21.0', '^3.21.0')).toBe(true) expect(satisfiesRange('3.99.0', '^3.0.0')).toBe(true) + expect(satisfiesRange('4.0.0-beta.102', '^4.0.0-beta.102')).toBe(true) }) it('does not satisfy across major', () => { diff --git a/packages/@overeng/genie/src/sdk/mod.ts b/packages/@overeng/genie/src/sdk/mod.ts index 707701b853..c30189ea0c 100644 --- a/packages/@overeng/genie/src/sdk/mod.ts +++ b/packages/@overeng/genie/src/sdk/mod.ts @@ -1,9 +1,10 @@ import path from 'node:path' -import { type Error as PlatformError, FileSystem } from '@effect/platform' -import type * as CommandExecutor from '@effect/platform/CommandExecutor' -import type { Path } from '@effect/platform/Path' +import type { Error as PlatformError } from 'effect' +import * as FileSystem from 'effect/FileSystem' import { Effect, Option, PubSub } from 'effect' +import type { Path } from 'effect/Path' +import type { ChildProcessSpawner } from 'effect/unstable/process' import { type GenieGenerateResult, @@ -99,7 +100,7 @@ export const generate = ({ }: GenieGenerateOptions): Effect.Effect< GenieGenerateResult, GenieGenerationFailedError | PlatformError.PlatformError, - FileSystem.FileSystem | Path | CommandExecutor.CommandExecutor + FileSystem.FileSystem | Path | ChildProcessSpawner.ChildProcessSpawner > => { const core = Effect.gen(function* () { const cwd = yield* resolveCwd(inputCwd) @@ -127,7 +128,7 @@ export const check = ({ }: GenieCheckOptions): Effect.Effect< void, GenieGenerationFailedError | PlatformError.PlatformError, - FileSystem.FileSystem | Path | CommandExecutor.CommandExecutor + FileSystem.FileSystem | Path | ChildProcessSpawner.ChildProcessSpawner > => { const core = Effect.gen(function* () { const cwd = yield* resolveCwd(inputCwd) diff --git a/packages/@overeng/kdl-effect/package.json b/packages/@overeng/kdl-effect/package.json index b94d2a330c..b4315c47f0 100644 --- a/packages/@overeng/kdl-effect/package.json +++ b/packages/@overeng/kdl-effect/package.json @@ -16,15 +16,15 @@ "@overeng/kdl": "workspace:^" }, "devDependencies": { - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/kdl-effect/src/decode.ts b/packages/@overeng/kdl-effect/src/decode.ts index 17e6693360..f67b704830 100644 --- a/packages/@overeng/kdl-effect/src/decode.ts +++ b/packages/@overeng/kdl-effect/src/decode.ts @@ -1,4 +1,4 @@ -import type { SchemaAST } from 'effect' +import { SchemaAST } from 'effect' import type { Document, Node } from '@overeng/kdl' @@ -82,51 +82,49 @@ const nodeToValue = (node: Node): unknown => { * into single-element arrays where the Schema expects an array. */ export const normalizeForSchema = (obj: unknown, ast: SchemaAST.AST): unknown => { - switch (ast._tag) { - case 'TupleType': { + const encodedAst = SchemaAST.toEncoded(ast) + + switch (encodedAst._tag) { + case 'Arrays': { const arr = Array.isArray(obj) ? obj : [obj] // Recurse into elements to normalize nested schemas - const elementType = ast.rest[0]?.type + const elementType = encodedAst.rest[0] if (elementType !== undefined) { return arr.map((item) => normalizeForSchema(item, elementType)) } return arr } - case 'TypeLiteral': { + case 'Objects': { if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return obj const record = obj as Record const result: Record = { ...record } // Normalize known property signatures - for (const prop of ast.propertySignatures) { + for (const prop of encodedAst.propertySignatures) { const key = prop.name if (typeof key === 'string' && key in result) { result[key] = normalizeForSchema(result[key], prop.type) } } // Normalize index signature (Record) values - for (const idx of ast.indexSignatures) { + for (const idx of encodedAst.indexSignatures) { for (const key of Object.keys(result)) { - if (ast.propertySignatures.some((p) => p.name === key)) continue + if (encodedAst.propertySignatures.some((p) => p.name === key)) continue result[key] = normalizeForSchema(result[key], idx.type) } } return result } - case 'Transformation': { - return normalizeForSchema(obj, ast.from) - } - case 'Union': { // Try to find a matching member. For normalization purposes, - // we pick the first member that is a TypeLiteral or TupleType + // we pick the first member that is an Objects or Arrays node // and apply normalization based on it. - for (const member of ast.types) { + for (const member of encodedAst.types) { if ( - member._tag === 'TupleType' || - member._tag === 'TypeLiteral' || - member._tag === 'Transformation' + member._tag === 'Arrays' || + member._tag === 'Objects' || + member.encoding !== undefined ) { return normalizeForSchema(obj, member) } @@ -135,16 +133,12 @@ export const normalizeForSchema = (obj: unknown, ast: SchemaAST.AST): unknown => } case 'Suspend': { - return normalizeForSchema(obj, ast.f()) - } - - case 'Refinement': { - return normalizeForSchema(obj, ast.from) + return normalizeForSchema(obj, encodedAst.thunk()) } case 'Declaration': { // Declarations like Schema.Array use typeParameters - if (ast.typeParameters.length > 0) { + if (encodedAst.typeParameters.length > 0) { // Check if this is array-like by seeing if the input should be an array if (!Array.isArray(obj)) { return [obj] diff --git a/packages/@overeng/kdl-effect/src/parse.test.ts b/packages/@overeng/kdl-effect/src/parse.test.ts index 511ee54cad..f8025de859 100644 --- a/packages/@overeng/kdl-effect/src/parse.test.ts +++ b/packages/@overeng/kdl-effect/src/parse.test.ts @@ -1,6 +1,8 @@ import { Schema } from 'effect' import { describe, expect, it } from 'vitest' +import { InvalidKdlError } from '@overeng/kdl' + import { parseKdl } from './parse.ts' describe('parseKdl', () => { @@ -8,6 +10,7 @@ describe('parseKdl', () => { it('decodes simple struct', () => { const MySchema = Schema.Struct({ name: Schema.String, + // @effect-diagnostics-next-line schemaNumber:off -- preserve the v3 parseKdl test's unrestricted numeric schema domain during the behavior-neutral port. age: Schema.Number, }) @@ -17,7 +20,7 @@ describe('parseKdl', () => { it('decodes nested struct', () => { const MySchema = Schema.Struct({ - members: Schema.Record({ key: Schema.String, value: Schema.String }), + members: Schema.Record(Schema.String, Schema.String), }) const result = Schema.decodeUnknownSync(parseKdl(MySchema))( @@ -44,17 +47,18 @@ describe('parseKdl', () => { expect(result).toEqual({ items: ['a', 'b', 'c'] }) }) - it('converts KDL parse errors to ParseError (not thrown)', () => { + it('converts KDL parse errors to SchemaError (not thrown raw)', () => { const MySchema = Schema.Struct({ name: Schema.String }) expect(() => Schema.decodeUnknownSync(parseKdl(MySchema))('{')).toThrow() - /* Verify it's a ParseError, not an InvalidKdlError */ + /* Verify the raw KDL parser error is converted into Effect's schema error channel. */ try { Schema.decodeUnknownSync(parseKdl(MySchema))('{') } catch (e) { expect(e).toBeInstanceOf(Error) - expect((e as Error).name).toBe('ParseError') + expect((e as Error).name).toBe('SchemaError') + expect(e).not.toBeInstanceOf(InvalidKdlError) } }) @@ -71,6 +75,7 @@ describe('parseKdl', () => { it('decodes optional fields', () => { const MySchema = Schema.Struct({ name: Schema.String, + // @effect-diagnostics-next-line schemaNumber:off -- preserve the v3 parseKdl test's unrestricted numeric schema domain during the behavior-neutral port. age: Schema.optional(Schema.Number), }) @@ -83,6 +88,7 @@ describe('parseKdl', () => { it('encodes simple struct to KDL', () => { const MySchema = Schema.Struct({ name: Schema.String, + // @effect-diagnostics-next-line schemaNumber:off -- preserve the v3 parseKdl test's unrestricted numeric schema domain during the behavior-neutral port. age: Schema.Number, }) @@ -96,7 +102,7 @@ describe('parseKdl', () => { it('round-trips nested struct', () => { const MySchema = Schema.Struct({ - members: Schema.Record({ key: Schema.String, value: Schema.String }), + members: Schema.Record(Schema.String, Schema.String), }) const original = { members: { foo: 'bar', baz: 'qux' } } @@ -131,7 +137,7 @@ describe('parseKdl', () => { describe('edge cases', () => { it('decodes empty children block as empty object', () => { const MySchema = Schema.Struct({ - settings: Schema.Record({ key: Schema.String, value: Schema.Unknown }), + settings: Schema.Record(Schema.String, Schema.Unknown), }) const result = Schema.decodeUnknownSync(parseKdl(MySchema))('settings {}') @@ -160,7 +166,7 @@ describe('parseKdl', () => { it('normalizes record values with array schemas', () => { const MySchema = Schema.Struct({ - groups: Schema.Record({ key: Schema.String, value: Schema.Array(Schema.String) }), + groups: Schema.Record(Schema.String, Schema.Array(Schema.String)), }) const result = Schema.decodeUnknownSync(parseKdl(MySchema))('groups {\n admin "alice"\n}') diff --git a/packages/@overeng/kdl-effect/src/parse.ts b/packages/@overeng/kdl-effect/src/parse.ts index 75cb83d39a..843b47ac99 100644 --- a/packages/@overeng/kdl-effect/src/parse.ts +++ b/packages/@overeng/kdl-effect/src/parse.ts @@ -1,4 +1,4 @@ -import { ParseResult, Schema } from 'effect' +import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from 'effect' import { format, parse } from '@overeng/kdl' @@ -8,53 +8,65 @@ import { objectToKdlDocument } from './encode.ts' /** * Base KDL transformation: `string ↔ unknown` * - * Analogous to how `Schema.parseJson()` wraps `JSON.parse`/`JSON.stringify`, + * Analogous to how `Schema.fromJsonString(Schema.Unknown)` wraps `JSON.parse`/`JSON.stringify`, * this wraps `parse`/`format` from `@overeng/kdl`. * - * Parse errors are mapped to `ParseResult.Type` issues (not thrown), + * Parse errors are mapped to `SchemaIssue.InvalidValue` issues (not thrown), * following the same pattern as Effect's `Schema.parseJson`. */ -const ParseKdl = Schema.transformOrFail( - Schema.String.annotations({ description: 'a KDL string to be decoded' }), - Schema.Unknown, - { - strict: true, - decode: (text, _, ast) => - ParseResult.try({ - try: () => { - const doc = parse(text) - return kdlToObject(doc) - }, - catch: (e) => new ParseResult.Type(ast, text, e instanceof Error ? e.message : String(e)), +const ParseKdl = Schema.String.annotate({ + description: 'a KDL string to be decoded', +}) + .pipe( + Schema.decodeTo( + Schema.Unknown, + SchemaTransformation.transformOrFail({ + decode: (text) => + Effect.try({ + try: () => { + const doc = parse(text) + return kdlToObject(doc) + }, + catch: (e) => + new SchemaIssue.InvalidValue(Option.some(text), { + message: e instanceof Error ? e.message : String(e), + }), + }), + encode: (value) => + Effect.try({ + try: () => { + const doc = objectToKdlDocument(value as Record) + return format(doc) + }, + catch: (e) => + new SchemaIssue.InvalidValue(Option.some(value), { + message: e instanceof Error ? e.message : String(e), + }), + }), }), - encode: (value, _, ast) => - ParseResult.try({ - try: () => { - const doc = objectToKdlDocument(value as Record) - return format(doc) - }, - catch: (e) => new ParseResult.Type(ast, value, e instanceof Error ? e.message : String(e)), - }), - }, -).annotations({ title: 'parseKdl' }) + ), + ) + .annotate({ title: 'parseKdl' }) /** * Create a Schema that decodes KDL text into the target type. - * Analogous to `Schema.parseJson(schema)`. + * Analogous to `Schema.fromJsonString(schema)`. * - * Uses `Schema.compose` to chain: `string → unknown` (via KDL parse) then `unknown → A` (via schema). - * Array normalization is applied between the two steps using the target schema's AST. + * Chains `string → unknown` (via KDL parse), normalized `unknown → unknown`, + * then `unknown → A` (via the target schema). */ -export const parseKdl = (schema: Schema.Schema): Schema.Schema => { - const normalizedKdl = Schema.transformOrFail(Schema.Unknown, Schema.Unknown, { - strict: true, - decode: (raw) => ParseResult.succeed(normalizeForSchema(raw, schema.ast)), - encode: (value) => ParseResult.succeed(value), - }) +export const parseKdl = ( + schema: Schema.Codec, +): Schema.Codec => { + const normalizedKdl = Schema.Unknown.pipe( + Schema.decodeTo( + Schema.Unknown, + SchemaTransformation.transform({ + decode: (raw) => normalizeForSchema(raw, schema.ast), + encode: (value) => value, + }), + ), + ) - return Schema.compose(Schema.compose(ParseKdl, normalizedKdl), schema) as Schema.Schema< - A, - string, - R - > + return ParseKdl.pipe(Schema.decodeTo(normalizedKdl), Schema.decodeTo(schema)) } diff --git a/packages/@overeng/kdl/package.json b/packages/@overeng/kdl/package.json index 2df75dc7da..6a0a1bb87e 100644 --- a/packages/@overeng/kdl/package.json +++ b/packages/@overeng/kdl/package.json @@ -13,15 +13,15 @@ } }, "devDependencies": { - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/kdl/src/error.ts b/packages/@overeng/kdl/src/error.ts index b60b42eb99..7d1bbbb8bf 100644 --- a/packages/@overeng/kdl/src/error.ts +++ b/packages/@overeng/kdl/src/error.ts @@ -2,19 +2,22 @@ import { Schema } from 'effect' /** Source location within a KDL document */ export const KdlLocation = Schema.Struct({ + // @effect-diagnostics-next-line schemaNumber:off -- preserve the public v3 schema's unrestricted numeric input domain during the behavior-neutral port. offset: Schema.Number, + // @effect-diagnostics-next-line schemaNumber:off -- preserve the public v3 schema's unrestricted numeric input domain during the behavior-neutral port. line: Schema.Number, + // @effect-diagnostics-next-line schemaNumber:off -- preserve the public v3 schema's unrestricted numeric input domain during the behavior-neutral port. column: Schema.Number, -}).annotations({ identifier: 'Kdl.Location' }) +}).annotate({ identifier: 'Kdl.Location' }) export type KdlLocation = typeof KdlLocation.Type /** Error thrown when invalid KDL is encountered */ -export class KdlParseError extends Schema.TaggedError()('KdlParseError', { +export class KdlParseError extends Schema.TaggedErrorClass()('KdlParseError', { message: Schema.String, - start: Schema.optionalWith(KdlLocation, { as: 'Option' }), - end: Schema.optionalWith(KdlLocation, { as: 'Option' }), - errors: Schema.optionalWith(Schema.Array(Schema.Unknown), { as: 'Option' }), + start: Schema.OptionFromOptional(KdlLocation), + end: Schema.OptionFromOptional(KdlLocation), + errors: Schema.OptionFromOptional(Schema.Array(Schema.Unknown)), }) { /** Iterate over all leaf errors (flattening nested error collections) */ *flat(): Generator { diff --git a/packages/@overeng/megarepo/bin/mr.ts b/packages/@overeng/megarepo/bin/mr.ts index 3a27fd44c7..f00e6ab46e 100644 --- a/packages/@overeng/megarepo/bin/mr.ts +++ b/packages/@overeng/megarepo/bin/mr.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun -import * as Cli from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import * as Cli from 'effect/unstable/cli' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect, Layer, Schema } from 'effect' import { ServiceIdentity } from '@overeng/otel-contract' @@ -75,7 +75,7 @@ const program = Effect.gen(function* () { Effect.scoped, CliVersion.enrichErrors, Effect.provideService(CliVersion, { name: 'mr', version }), - Effect.provide(Layer.mergeAll(NodeContext.layer, otelLayer)), + Effect.provide(Layer.mergeAll(NodeServices.layer, otelLayer)), ) }) diff --git a/packages/@overeng/megarepo/nix/build.nix b/packages/@overeng/megarepo/nix/build.nix index 0c0413e4c3..f9498b52a8 100644 --- a/packages/@overeng/megarepo/nix/build.nix +++ b/packages/@overeng/megarepo/nix/build.nix @@ -24,7 +24,7 @@ let workspaceRoot = src; # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { - "." = mkSharedHash "sha256-qC8Zwf/5v9EOEplQe7nYC0ZsbIBqBPun5Qo/cUEsiDo="; + "." = mkSharedHash "sha256-Ac9B0YICoQYm9zNUHRtNdIncdo+p0aDe+XuutGFOSxU="; }; nativeNodePackages = opentuiCoreNative.packages; smokeTestArgs = [ "--help" ]; diff --git a/packages/@overeng/megarepo/package.json b/packages/@overeng/megarepo/package.json index 1523c1d52b..3c657a01a8 100644 --- a/packages/@overeng/megarepo/package.json +++ b/packages/@overeng/megarepo/package.json @@ -19,17 +19,10 @@ "storybook:build": "storybook build" }, "dependencies": { - "@effect-atom/atom": "0.5.3", - "@effect-atom/atom-react": "0.5.0", - "@effect/cli": "0.75.2", - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/atom-react": "4.0.0-beta.102", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@opentui/core": "0.4.1", "@opentui/react": "0.4.1", "@overeng/effect-path": "workspace:^", @@ -42,19 +35,15 @@ "@storybook/react": "10.4.6", "@types/react": "19.2.17", "@types/react-reconciler": "0.33.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-dom": "19.2.7", "react-reconciler": "0.33.0", "vitest": "4.1.9" }, "devDependencies": { - "@effect/cli": "0.75.2", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/printer": "0.49.0", - "@effect/printer-ansi": "0.49.0", - "@effect/vitest": "0.29.0", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@overeng/tui-core": "workspace:^", "@overeng/utils-dev": "workspace:^", "@storybook/react": "10.4.6", @@ -65,7 +54,7 @@ "@vitejs/plugin-react": "6.0.2", "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "6.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react-dom": "19.2.7", "react-reconciler": "0.33.0", "storybook": "10.4.6", @@ -79,25 +68,16 @@ } }, "peerDependencies": { - "@effect-atom/atom": "^0.5.3", - "@effect-atom/atom-react": "^0.5.0", - "@effect/cli": "^0.75.2", - "@effect/cluster": "^0.59.0", - "@effect/experimental": "^0.60.0", - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/printer": "^0.49.0", - "@effect/printer-ansi": "^0.49.0", - "@effect/rpc": "^0.75.1", - "@effect/workflow": "^0.18.2", + "@effect/atom-react": "^4.0.0-beta.102", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@opentui/core": "^0.4.1", "@opentui/react": "^0.4.1", "@playwright/test": "^1.61.0", "@storybook/react": "^10.4.6", "@types/react": "^19.2.17", "@types/react-reconciler": "^0.33.0", - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "react": "^19.2.7", "react-dom": "^19.2.7", "react-reconciler": "^0.33.0" diff --git a/packages/@overeng/megarepo/package.json.genie.ts b/packages/@overeng/megarepo/package.json.genie.ts index 7878614086..ac25217f14 100644 --- a/packages/@overeng/megarepo/package.json.genie.ts +++ b/packages/@overeng/megarepo/package.json.genie.ts @@ -15,14 +15,7 @@ import tuiReactPkg from '../tui-react/package.json.genie.ts' import utilsDevPkg from '../utils-dev/package.json.genie.ts' import utilsPkg from '../utils/package.json.genie.ts' -const peerDepNames = [ - '@effect/cli', - '@effect/platform', - '@effect/platform-node', - '@effect/printer', - '@effect/printer-ansi', - 'effect', -] as const +const peerDepNames = ['effect', '@effect/platform-node'] as const const runtimeDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/megarepo' }), @@ -33,24 +26,7 @@ const runtimeDeps = catalog.compose({ devDependencies: { workspace: [tuiCorePkg, utilsDevPkg], external: { - ...catalog.pick( - ...peerDepNames, - '@effect/vitest', - '@types/bun', - '@types/node', - '@types/react', - 'vitest', - 'storybook', - '@storybook/react', - '@storybook/react-vite', - '@xterm/xterm', - '@xterm/addon-fit', - 'react-dom', - 'react-reconciler', - 'typescript', - 'vite', - '@vitejs/plugin-react', - ), + ...catalog.pick(...peerDepNames, '@effect/vitest', '@types/bun', '@types/node', '@types/react', 'vitest', 'storybook', '@storybook/react', '@storybook/react-vite', '@xterm/xterm', '@xterm/addon-fit', 'react-dom', 'react-reconciler', 'typescript', 'vite', '@vitejs/plugin-react'), }, }, peerDependencies: { diff --git a/packages/@overeng/megarepo/src/cli/cli.integration.test.ts b/packages/@overeng/megarepo/src/cli/cli.integration.test.ts index 87c4f3552d..a23c0bc6bd 100644 --- a/packages/@overeng/megarepo/src/cli/cli.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/cli.integration.test.ts @@ -5,11 +5,11 @@ * These tests verify the core logic without invoking the CLI directly. */ -import * as Cli from '@effect/cli' -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' +import { FileSystem } from 'effect/FileSystem' import { Effect, Exit, Option, Schema } from 'effect' +import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -81,9 +81,9 @@ describe('mr init', () => { 'https://raw.githubusercontent.com/overengineeringstudio/megarepo/main/schema/megarepo.schema.json', members: {}, } - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - initialConfig, - ) + const configContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(initialConfig) yield* fs.writeFileString(configPath, configContent + '\n') // Verify config was created @@ -94,7 +94,7 @@ describe('mr init', () => { expect(config.members).toEqual({}) expect(config.$schema).toBeDefined() }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -119,9 +119,9 @@ describe('mr init', () => { workDir, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - existingConfig, - ) + const configContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(existingConfig) yield* fs.writeFileString(configPath, configContent + '\n') // Verify existing config @@ -133,7 +133,7 @@ describe('mr init', () => { const configAfter = yield* readConfig(workDir) expect(configAfter.members['existing-lib']).toBe('owner/existing-lib') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -167,7 +167,7 @@ describe('mr root', () => { expect(Option.isSome(root)).toBe(true) expect(Option.getOrNull(root)).toBe(workDir) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -196,7 +196,7 @@ describe('mr root', () => { expect(Option.isSome(root)).toBe(true) expect(Option.getOrNull(root)).toBe(workDir) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -235,7 +235,7 @@ describe('mr root', () => { expect(Option.isSome(root)).toBe(true) expect(Option.getOrNull(root)).toBe(outerDir) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -255,7 +255,7 @@ describe('mr root', () => { const root = yield* findMegarepoRoot(workDir) expect(Option.isNone(root)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -283,9 +283,9 @@ describe('mr add', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const initialConfig: MegarepoConfig = { members: {} } - const initialContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - initialConfig, - ) + const initialContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(initialConfig) yield* fs.writeFileString(configPath, initialContent + '\n') // Add a member @@ -298,16 +298,16 @@ describe('mr add', () => { ...config, members: { ...config.members, [memberName]: memberSource }, } - const updatedContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - updatedConfig, - ) + const updatedContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(updatedConfig) yield* fs.writeFileString(configPath, updatedContent + '\n') // Verify member was added const finalConfig = yield* readConfig(workDir) expect(finalConfig.members['effect']).toBe('effect-ts/effect') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -339,15 +339,15 @@ describe('mr add', () => { ...config, members: { ...config.members, [customName]: memberSource }, } - const updatedContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - updatedConfig, - ) + const updatedContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(updatedConfig) yield* fs.writeFileString(configPath, updatedContent + '\n') const finalConfig = yield* readConfig(workDir) expect(finalConfig.members['effect-v3']).toBe('effect-ts/effect#v3.0.0') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -379,9 +379,9 @@ describe('mr add', () => { const initialConfig: MegarepoConfig = { members: { effect: 'effect-ts/effect' }, } - const initialContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - initialConfig, - ) + const initialContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(initialConfig) yield* fs.writeFileString(configPath, initialContent + '\n') // Check that member already exists @@ -391,7 +391,7 @@ describe('mr add', () => { // In real CLI, this would fail with "Member already exists" expect(memberName in config.members).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -428,7 +428,9 @@ describe('megarepo.json parsing', () => { local: './packages/local', }, } - const content = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(config) + const content = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + config, + ) yield* fs.writeFileString(configPath, content + '\n') const parsed = yield* readConfig(workDir) @@ -437,7 +439,7 @@ describe('megarepo.json parsing', () => { expect(parsed.members['github-ref']).toBe('owner/repo#main') expect(parsed.members['local']).toBe('./packages/local') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -463,14 +465,16 @@ describe('megarepo.json parsing', () => { vscode: { enabled: true, exclude: ['large-repo'] }, }, } - const content = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(config) + const content = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + config, + ) yield* fs.writeFileString(configPath, content + '\n') const parsed = yield* readConfig(workDir) expect(parsed.generators?.vscode?.enabled).toBe(true) expect(parsed.generators?.vscode?.exclude).toEqual(['large-repo']) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -501,7 +505,7 @@ const runRootWithCwd = ({ cwdPath }: { cwdPath: string }) => let state: RootState | undefined if (stdout.trim() !== '') { - state = yield* Schema.decodeUnknown(Schema.parseJson(RootState))(stdout) + state = yield* Schema.decodeUnknown(Schema.fromJsonString(RootState))(stdout) } return { @@ -538,7 +542,7 @@ describe('--cwd option', () => { expect(state!.root).toBe(workDir) } }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -558,7 +562,7 @@ describe('--cwd option', () => { expect(state).toBeDefined() expect(state!._tag).toBe('Error') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -578,7 +582,7 @@ describe('--cwd option', () => { expect(Exit.isFailure(exit)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/cli/commands/add.ts b/packages/@overeng/megarepo/src/cli/commands/add.ts index 66f0c324c3..ce37c002dc 100644 --- a/packages/@overeng/megarepo/src/cli/commands/add.ts +++ b/packages/@overeng/megarepo/src/cli/commands/add.ts @@ -4,8 +4,8 @@ * Adds a new member repository to the megarepo configuration. */ -import * as Cli from '@effect/cli' import { Effect, Option } from 'effect' +import * as Cli from 'effect/unstable/cli' import React from 'react' import { run } from '@overeng/tui-react' @@ -58,18 +58,18 @@ const parseRepoRef = (ref: string): { sourceString: string; suggestedName: strin export const addCommand = Cli.Command.make( 'add', { - repo: Cli.Args.text({ name: 'repo' }).pipe( - Cli.Args.withDescription('Repository reference (github shorthand, URL, or path)'), + repo: Cli.Argument.string('repo').pipe( + Cli.Argument.withDescription('Repository reference (github shorthand, URL, or path)'), ), - name: Cli.Options.text('name').pipe( - Cli.Options.withAlias('n'), - Cli.Options.withDescription('Override the member name (defaults to repo name)'), - Cli.Options.optional, + name: Cli.Flag.string('name').pipe( + Cli.Flag.withAlias('n'), + Cli.Flag.withDescription('Override the member name (defaults to repo name)'), + Cli.Flag.optional, ), - sync: Cli.Options.boolean('sync').pipe( - Cli.Options.withAlias('s'), - Cli.Options.withDescription('Sync the added repo immediately'), - Cli.Options.withDefault(true), + sync: Cli.Flag.boolean('sync').pipe( + Cli.Flag.withAlias('s'), + Cli.Flag.withDescription('Sync the added repo immediately'), + Cli.Flag.withDefault(true), ), output: outputOption, }, diff --git a/packages/@overeng/megarepo/src/cli/commands/apply.ts b/packages/@overeng/megarepo/src/cli/commands/apply.ts index 13706da89f..628723b3bc 100644 --- a/packages/@overeng/megarepo/src/cli/commands/apply.ts +++ b/packages/@overeng/megarepo/src/cli/commands/apply.ts @@ -4,16 +4,16 @@ * Create worktrees from lock, symlink, nix lock sync, generators. Never writes lock. */ -import * as Cli from '@effect/cli' +import * as Cli from 'effect/unstable/cli' import { outputOption, verboseOption } from '../context.ts' import { runCommand, type LockSyncMode } from './engine.ts' -const lockSyncOption = Cli.Options.choice('lock-sync', ['auto', 'off', 'direct', 'recursive']).pipe( - Cli.Options.withDescription( +const lockSyncOption = Cli.Flag.choice('lock-sync', ['auto', 'off', 'direct', 'recursive']).pipe( + Cli.Flag.withDescription( 'Lock-file rewrite policy during apply: auto, off, direct members only, or recursive nested megarepos', ), - Cli.Options.withDefault('auto' as LockSyncMode), + Cli.Flag.withDefault('auto' as LockSyncMode), ) /** `mr apply` — Lock → Workspace: create worktrees, symlink, nix lock sync, generators. */ @@ -21,38 +21,38 @@ export const applyCommand = Cli.Command.make( 'apply', { output: outputOption, - dryRun: Cli.Options.boolean('dry-run').pipe( - Cli.Options.withDescription('Show what would be done without making changes'), - Cli.Options.withDefault(false), + dryRun: Cli.Flag.boolean('dry-run').pipe( + Cli.Flag.withDescription('Show what would be done without making changes'), + Cli.Flag.withDefault(false), ), - force: Cli.Options.boolean('force').pipe( - Cli.Options.withAlias('f'), - Cli.Options.withDescription('Force updates for pinned members'), - Cli.Options.withDefault(false), + force: Cli.Flag.boolean('force').pipe( + Cli.Flag.withAlias('f'), + Cli.Flag.withDescription('Force updates for pinned members'), + Cli.Flag.withDefault(false), ), - all: Cli.Options.boolean('all').pipe( - Cli.Options.withDescription('Recursively apply nested megarepos'), - Cli.Options.withDefault(false), + all: Cli.Flag.boolean('all').pipe( + Cli.Flag.withDescription('Recursively apply nested megarepos'), + Cli.Flag.withDefault(false), ), - only: Cli.Options.text('only').pipe( - Cli.Options.withDescription('Only apply specified members (comma-separated)'), - Cli.Options.optional, + only: Cli.Flag.string('only').pipe( + Cli.Flag.withDescription('Only apply specified members (comma-separated)'), + Cli.Flag.optional, ), - skip: Cli.Options.text('skip').pipe( - Cli.Options.withDescription('Skip specified members (comma-separated)'), - Cli.Options.optional, + skip: Cli.Flag.string('skip').pipe( + Cli.Flag.withDescription('Skip specified members (comma-separated)'), + Cli.Flag.optional, ), - gitProtocol: Cli.Options.choice('git-protocol', ['ssh', 'https', 'auto']).pipe( - Cli.Options.withDescription( + gitProtocol: Cli.Flag.choice('git-protocol', ['ssh', 'https', 'auto']).pipe( + Cli.Flag.withDescription( 'Git protocol for cloning: ssh (default for new clones), https, or auto (use lock file URL if available)', ), - Cli.Options.withDefault('auto' as const), + Cli.Flag.withDefault('auto' as const), ), - worktreeMode: Cli.Options.choice('worktree-mode', ['commit', 'tracking', 'auto']).pipe( - Cli.Options.withDescription( + worktreeMode: Cli.Flag.choice('worktree-mode', ['commit', 'tracking', 'auto']).pipe( + Cli.Flag.withDescription( 'Worktree strategy: commit (deterministic), tracking (branch worktrees), auto (commit in CI, tracking otherwise)', ), - Cli.Options.withDefault('auto' as const), + Cli.Flag.withDefault('auto' as const), ), lockSync: lockSyncOption, verbose: verboseOption, diff --git a/packages/@overeng/megarepo/src/cli/commands/check.ts b/packages/@overeng/megarepo/src/cli/commands/check.ts index dd76bfde93..1969c502b5 100644 --- a/packages/@overeng/megarepo/src/cli/commands/check.ts +++ b/packages/@overeng/megarepo/src/cli/commands/check.ts @@ -1,5 +1,5 @@ -import * as Cli from '@effect/cli' import { Console, Effect, Option, Schema } from 'effect' +import * as Cli from 'effect/unstable/cli' import { EffectPath } from '@overeng/effect-path' @@ -11,11 +11,11 @@ import { CheckCommandError, LockFileRequiredError, NotInMegarepoError } from '.. import * as Observability from '../observability.ts' /** Encodes the structured check result as pretty-printed JSON for `--json` output. */ -const CheckReportJson = Schema.parseJson(Schema.Unknown, { space: 2 }) +const CheckReportJson = Schema.fromJsonString(Schema.Unknown, { space: 2 }) -const allOption = Cli.Options.boolean('all').pipe( - Cli.Options.withDescription('Check member source and lock files in repos/ as well as the root'), - Cli.Options.withDefault(false), +const allOption = Cli.Flag.boolean('all').pipe( + Cli.Flag.withDescription('Check member source and lock files in repos/ as well as the root'), + Cli.Flag.withDefault(false), ) /** Check that the megarepo is structurally valid. */ diff --git a/packages/@overeng/megarepo/src/cli/commands/config/mod.ts b/packages/@overeng/megarepo/src/cli/commands/config/mod.ts index 8fb1a69379..e4b9880bff 100644 --- a/packages/@overeng/megarepo/src/cli/commands/config/mod.ts +++ b/packages/@overeng/megarepo/src/cli/commands/config/mod.ts @@ -2,7 +2,7 @@ * Config subcommand group — push-refs, pin, unpin */ -import * as Cli from '@effect/cli' +import * as Cli from 'effect/unstable/cli' import { pinCommand, unpinCommand } from '../pin.ts' import { pushRefsCommand } from './push-refs.ts' diff --git a/packages/@overeng/megarepo/src/cli/commands/config/push-refs.ts b/packages/@overeng/megarepo/src/cli/commands/config/push-refs.ts index d6e94ef671..f4f3eec6ff 100644 --- a/packages/@overeng/megarepo/src/cli/commands/config/push-refs.ts +++ b/packages/@overeng/megarepo/src/cli/commands/config/push-refs.ts @@ -6,9 +6,9 @@ * Matching is done by canonical URL (org/repo), not by member name. */ -import * as Cli from '@effect/cli' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option } from 'effect' +import * as Cli from 'effect/unstable/cli' import React from 'react' import type { AbsoluteDirPath } from '@overeng/effect-path' @@ -153,17 +153,17 @@ export const pushRefsCommand = Cli.Command.make( 'push-refs', { output: outputOption, - dryRun: Cli.Options.boolean('dry-run').pipe( - Cli.Options.withDescription('Show what would change without writing files'), - Cli.Options.withDefault(false), + dryRun: Cli.Flag.boolean('dry-run').pipe( + Cli.Flag.withDescription('Show what would change without writing files'), + Cli.Flag.withDefault(false), ), - all: Cli.Options.boolean('all').pipe( - Cli.Options.withDescription('Recursively push refs to nested-of-nested megarepos'), - Cli.Options.withDefault(false), + all: Cli.Flag.boolean('all').pipe( + Cli.Flag.withDescription('Recursively push refs to nested-of-nested megarepos'), + Cli.Flag.withDefault(false), ), - only: Cli.Options.text('only').pipe( - Cli.Options.withDescription('Only push refs for these shared members (comma-separated)'), - Cli.Options.optional, + only: Cli.Flag.string('only').pipe( + Cli.Flag.withDescription('Only push refs for these shared members (comma-separated)'), + Cli.Flag.optional, ), }, ({ output, dryRun, all, only }) => diff --git a/packages/@overeng/megarepo/src/cli/commands/deps.ts b/packages/@overeng/megarepo/src/cli/commands/deps.ts index cad5535b25..6f47c48284 100644 --- a/packages/@overeng/megarepo/src/cli/commands/deps.ts +++ b/packages/@overeng/megarepo/src/cli/commands/deps.ts @@ -4,7 +4,7 @@ * Show the Nix input dependency graph between megarepo members. */ -import * as Cli from '@effect/cli' +import * as Cli from 'effect/unstable/cli' import { Effect, Option } from 'effect' import React from 'react' diff --git a/packages/@overeng/megarepo/src/cli/commands/engine.ts b/packages/@overeng/megarepo/src/cli/commands/engine.ts index 66eabcfb72..b274aa8ae6 100644 --- a/packages/@overeng/megarepo/src/cli/commands/engine.ts +++ b/packages/@overeng/megarepo/src/cli/commands/engine.ts @@ -6,9 +6,11 @@ * - `runCommand` — CLI orchestration: TUI rendering, fetch-before-apply, error merging */ -import { Prompt } from '@effect/cli' -import type { CommandExecutor, Terminal } from '@effect/platform' -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import { Prompt } from 'effect/unstable/cli' +import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' +import type { Terminal } from 'effect/Terminal' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Clock, Effect, Option, type ParseResult } from 'effect' import React from 'react' diff --git a/packages/@overeng/megarepo/src/cli/commands/env.ts b/packages/@overeng/megarepo/src/cli/commands/env.ts index a1ce9f0d6b..dad271d44e 100644 --- a/packages/@overeng/megarepo/src/cli/commands/env.ts +++ b/packages/@overeng/megarepo/src/cli/commands/env.ts @@ -4,8 +4,8 @@ * Print environment variables for shell integration. */ -import * as Cli from '@effect/cli' import { Effect } from 'effect' +import * as Cli from 'effect/unstable/cli' import React from 'react' import { run } from '@overeng/tui-react' @@ -19,9 +19,9 @@ import { EnvApp, EnvView } from '../renderers/EnvOutput/mod.ts' export const envCommand = Cli.Command.make( 'env', { - shell: Cli.Options.choice('shell', ['bash', 'zsh', 'fish']).pipe( - Cli.Options.withDescription('Shell type for output format'), - Cli.Options.withDefault('bash' as const), + shell: Cli.Flag.choice('shell', ['bash', 'zsh', 'fish']).pipe( + Cli.Flag.withDescription('Shell type for output format'), + Cli.Flag.withDefault('bash' as const), ), output: outputOption, }, diff --git a/packages/@overeng/megarepo/src/cli/commands/exec.ts b/packages/@overeng/megarepo/src/cli/commands/exec.ts index ecafe0232c..b02b8c50a2 100644 --- a/packages/@overeng/megarepo/src/cli/commands/exec.ts +++ b/packages/@overeng/megarepo/src/cli/commands/exec.ts @@ -4,9 +4,10 @@ * Execute a command in member directories. */ -import * as Cli from '@effect/cli' -import { Command, FileSystem } from '@effect/platform' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option } from 'effect' +import * as Cli from 'effect/unstable/cli' import React from 'react' import { run } from '@overeng/tui-react' @@ -23,18 +24,18 @@ type ExecMode = 'parallel' | 'sequential' export const execCommand = Cli.Command.make( 'exec', { - command: Cli.Args.text({ name: 'command' }).pipe( - Cli.Args.withDescription('Command to execute'), + command: Cli.Argument.string('command').pipe( + Cli.Argument.withDescription('Command to execute'), ), output: outputOption, - member: Cli.Options.text('member').pipe( - Cli.Options.withAlias('m'), - Cli.Options.withDescription('Run only in this member'), - Cli.Options.optional, + member: Cli.Flag.string('member').pipe( + Cli.Flag.withAlias('m'), + Cli.Flag.withDescription('Run only in this member'), + Cli.Flag.optional, ), - mode: Cli.Options.choice('mode', ['parallel', 'sequential'] as const).pipe( - Cli.Options.withDescription('Execution mode: parallel (default) or sequential'), - Cli.Options.withDefault('parallel' as ExecMode), + mode: Cli.Flag.choice('mode', ['parallel', 'sequential'] as const).pipe( + Cli.Flag.withDescription('Execution mode: parallel (default) or sequential'), + Cli.Flag.withDefault('parallel' as ExecMode), ), verbose: verboseOption, }, diff --git a/packages/@overeng/megarepo/src/cli/commands/fetch.ts b/packages/@overeng/megarepo/src/cli/commands/fetch.ts index 33bdbd4cbc..e41d5c3b1d 100644 --- a/packages/@overeng/megarepo/src/cli/commands/fetch.ts +++ b/packages/@overeng/megarepo/src/cli/commands/fetch.ts @@ -5,52 +5,52 @@ * With `--apply`, also applies lock to workspace afterward. */ -import * as Cli from '@effect/cli' +import * as Cli from 'effect/unstable/cli' import { outputOption, verboseOption } from '../context.ts' import { runCommand, type LockSyncMode } from './engine.ts' -const lockSyncOption = Cli.Options.choice('lock-sync', ['auto', 'off', 'direct', 'recursive']).pipe( - Cli.Options.withDescription( +const lockSyncOption = Cli.Flag.choice('lock-sync', ['auto', 'off', 'direct', 'recursive']).pipe( + Cli.Flag.withDescription( 'Lock-file rewrite policy for the apply phase: auto, off, direct members only, or recursive nested megarepos', ), - Cli.Options.withDefault('auto' as LockSyncMode), + Cli.Flag.withDefault('auto' as LockSyncMode), ) const sharedOptions = { output: outputOption, - dryRun: Cli.Options.boolean('dry-run').pipe( - Cli.Options.withDescription('Show what would be done without making changes'), - Cli.Options.withDefault(false), + dryRun: Cli.Flag.boolean('dry-run').pipe( + Cli.Flag.withDescription('Show what would be done without making changes'), + Cli.Flag.withDefault(false), ), - force: Cli.Options.boolean('force').pipe( - Cli.Options.withAlias('f'), - Cli.Options.withDescription('Force lock operation even when members are pinned or need repair'), - Cli.Options.withDefault(false), + force: Cli.Flag.boolean('force').pipe( + Cli.Flag.withAlias('f'), + Cli.Flag.withDescription('Force lock operation even when members are pinned or need repair'), + Cli.Flag.withDefault(false), ), - all: Cli.Options.boolean('all').pipe( - Cli.Options.withDescription('Recursively operate on nested megarepos'), - Cli.Options.withDefault(false), + all: Cli.Flag.boolean('all').pipe( + Cli.Flag.withDescription('Recursively operate on nested megarepos'), + Cli.Flag.withDefault(false), ), - only: Cli.Options.text('only').pipe( - Cli.Options.withDescription('Only operate on specified members (comma-separated)'), - Cli.Options.optional, + only: Cli.Flag.string('only').pipe( + Cli.Flag.withDescription('Only operate on specified members (comma-separated)'), + Cli.Flag.optional, ), - skip: Cli.Options.text('skip').pipe( - Cli.Options.withDescription('Skip specified members (comma-separated)'), - Cli.Options.optional, + skip: Cli.Flag.string('skip').pipe( + Cli.Flag.withDescription('Skip specified members (comma-separated)'), + Cli.Flag.optional, ), - gitProtocol: Cli.Options.choice('git-protocol', ['ssh', 'https', 'auto']).pipe( - Cli.Options.withDescription( + gitProtocol: Cli.Flag.choice('git-protocol', ['ssh', 'https', 'auto']).pipe( + Cli.Flag.withDescription( 'Git protocol for cloning: ssh (default for new clones), https, or auto (use lock file URL if available)', ), - Cli.Options.withDefault('auto' as const), + Cli.Flag.withDefault('auto' as const), ), - worktreeMode: Cli.Options.choice('worktree-mode', ['commit', 'tracking', 'auto']).pipe( - Cli.Options.withDescription( + worktreeMode: Cli.Flag.choice('worktree-mode', ['commit', 'tracking', 'auto']).pipe( + Cli.Flag.withDescription( 'Worktree strategy: commit (deterministic), tracking (branch worktrees), auto (commit in CI, tracking otherwise)', ), - Cli.Options.withDefault('auto' as const), + Cli.Flag.withDefault('auto' as const), ), lockSync: lockSyncOption, verbose: verboseOption, @@ -61,15 +61,15 @@ export const fetchCommand = Cli.Command.make( 'fetch', { ...sharedOptions, - apply: Cli.Options.boolean('apply').pipe( - Cli.Options.withDescription( + apply: Cli.Flag.boolean('apply').pipe( + Cli.Flag.withDescription( 'After fetching, also apply the lock to the workspace (fetch + apply)', ), - Cli.Options.withDefault(false), + Cli.Flag.withDefault(false), ), - createBranches: Cli.Options.boolean('create-branches').pipe( - Cli.Options.withDescription('Create branches that do not exist (from default branch)'), - Cli.Options.withDefault(false), + createBranches: Cli.Flag.boolean('create-branches').pipe( + Cli.Flag.withDescription('Create branches that do not exist (from default branch)'), + Cli.Flag.withDefault(false), ), }, ({ diff --git a/packages/@overeng/megarepo/src/cli/commands/generate/mod.ts b/packages/@overeng/megarepo/src/cli/commands/generate/mod.ts index bd9a18619b..6905829ba9 100644 --- a/packages/@overeng/megarepo/src/cli/commands/generate/mod.ts +++ b/packages/@overeng/megarepo/src/cli/commands/generate/mod.ts @@ -4,8 +4,8 @@ * Commands for generating configuration files. */ -import * as Cli from '@effect/cli' import { Effect, Option } from 'effect' +import * as Cli from 'effect/unstable/cli' import React from 'react' import { run } from '@overeng/tui-react' @@ -24,9 +24,9 @@ const generateVscodeCommand = Cli.Command.make( 'vscode', { output: outputOption, - exclude: Cli.Options.text('exclude').pipe( - Cli.Options.withDescription('Comma-separated list of members to exclude'), - Cli.Options.optional, + exclude: Cli.Flag.string('exclude').pipe( + Cli.Flag.withDescription('Comma-separated list of members to exclude'), + Cli.Flag.optional, ), }, ({ output, exclude }) => @@ -82,10 +82,10 @@ const generateSchemaCommand = Cli.Command.make( 'schema', { output: outputOption, - outputPath: Cli.Options.text('output-path').pipe( - Cli.Options.withAlias('p'), - Cli.Options.withDescription('Output path (relative to megarepo root)'), - Cli.Options.withDefault('schema/megarepo.schema.json'), + outputPath: Cli.Flag.string('output-path').pipe( + Cli.Flag.withAlias('p'), + Cli.Flag.withDescription('Output path (relative to megarepo root)'), + Cli.Flag.withDefault('schema/megarepo.schema.json'), ), }, ({ output, outputPath }) => diff --git a/packages/@overeng/megarepo/src/cli/commands/init.ts b/packages/@overeng/megarepo/src/cli/commands/init.ts index 68b5399224..3b6bf9c9d0 100644 --- a/packages/@overeng/megarepo/src/cli/commands/init.ts +++ b/packages/@overeng/megarepo/src/cli/commands/init.ts @@ -4,7 +4,7 @@ * Initialize a new megarepo in the current directory. */ -import * as Cli from '@effect/cli' +import * as Cli from 'effect/unstable/cli' import { Effect } from 'effect' import React from 'react' diff --git a/packages/@overeng/megarepo/src/cli/commands/lock.ts b/packages/@overeng/megarepo/src/cli/commands/lock.ts index d0dcb999c6..88244bf6c1 100644 --- a/packages/@overeng/megarepo/src/cli/commands/lock.ts +++ b/packages/@overeng/megarepo/src/cli/commands/lock.ts @@ -4,7 +4,7 @@ * Record current worktree HEAD commits into megarepo.lock. No network, no workspace changes. */ -import * as Cli from '@effect/cli' +import * as Cli from 'effect/unstable/cli' import { outputOption, verboseOption } from '../context.ts' import { runCommand } from './engine.ts' @@ -14,34 +14,32 @@ export const lockCommand = Cli.Command.make( 'lock', { output: outputOption, - dryRun: Cli.Options.boolean('dry-run').pipe( - Cli.Options.withDescription('Show what would be done without making changes'), - Cli.Options.withDefault(false), + dryRun: Cli.Flag.boolean('dry-run').pipe( + Cli.Flag.withDescription('Show what would be done without making changes'), + Cli.Flag.withDefault(false), ), - force: Cli.Options.boolean('force').pipe( - Cli.Options.withAlias('f'), - Cli.Options.withDescription( - 'Force lock operation even when members are pinned or need repair', - ), - Cli.Options.withDefault(false), + force: Cli.Flag.boolean('force').pipe( + Cli.Flag.withAlias('f'), + Cli.Flag.withDescription('Force lock operation even when members are pinned or need repair'), + Cli.Flag.withDefault(false), ), - all: Cli.Options.boolean('all').pipe( - Cli.Options.withDescription('Recursively operate on nested megarepos'), - Cli.Options.withDefault(false), + all: Cli.Flag.boolean('all').pipe( + Cli.Flag.withDescription('Recursively operate on nested megarepos'), + Cli.Flag.withDefault(false), ), - only: Cli.Options.text('only').pipe( - Cli.Options.withDescription('Only operate on specified members (comma-separated)'), - Cli.Options.optional, + only: Cli.Flag.string('only').pipe( + Cli.Flag.withDescription('Only operate on specified members (comma-separated)'), + Cli.Flag.optional, ), - skip: Cli.Options.text('skip').pipe( - Cli.Options.withDescription('Skip specified members (comma-separated)'), - Cli.Options.optional, + skip: Cli.Flag.string('skip').pipe( + Cli.Flag.withDescription('Skip specified members (comma-separated)'), + Cli.Flag.optional, ), - gitProtocol: Cli.Options.choice('git-protocol', ['ssh', 'https', 'auto']).pipe( - Cli.Options.withDescription( + gitProtocol: Cli.Flag.choice('git-protocol', ['ssh', 'https', 'auto']).pipe( + Cli.Flag.withDescription( 'Git protocol for cloning: ssh (default for new clones), https, or auto (use lock file URL if available)', ), - Cli.Options.withDefault('auto' as const), + Cli.Flag.withDefault('auto' as const), ), verbose: verboseOption, }, diff --git a/packages/@overeng/megarepo/src/cli/commands/ls.ts b/packages/@overeng/megarepo/src/cli/commands/ls.ts index 97644ecd97..312a818214 100644 --- a/packages/@overeng/megarepo/src/cli/commands/ls.ts +++ b/packages/@overeng/megarepo/src/cli/commands/ls.ts @@ -4,9 +4,10 @@ * List all members in the megarepo. */ -import * as Cli from '@effect/cli' -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option, type ParseResult } from 'effect' +import * as Cli from 'effect/unstable/cli' import React from 'react' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -118,9 +119,9 @@ export const lsCommand = Cli.Command.make( 'ls', { output: outputOption, - all: Cli.Options.boolean('all').pipe( - Cli.Options.withDescription('Recursively list members from nested megarepos'), - Cli.Options.withDefault(false), + all: Cli.Flag.boolean('all').pipe( + Cli.Flag.withDescription('Recursively list members from nested megarepos'), + Cli.Flag.withDefault(false), ), }, ({ output, all }) => diff --git a/packages/@overeng/megarepo/src/cli/commands/pin.ts b/packages/@overeng/megarepo/src/cli/commands/pin.ts index 84333764f5..1568898113 100644 --- a/packages/@overeng/megarepo/src/cli/commands/pin.ts +++ b/packages/@overeng/megarepo/src/cli/commands/pin.ts @@ -4,9 +4,9 @@ * Commands to pin and unpin members to specific refs. */ -import * as Cli from '@effect/cli' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Clock, Effect, Layer, Option } from 'effect' +import * as Cli from 'effect/unstable/cli' import React from 'react' import { EffectPath } from '@overeng/effect-path' @@ -59,15 +59,15 @@ import { PinApp, PinView } from '../renderers/PinOutput/mod.ts' export const pinCommand = Cli.Command.make( 'pin', { - member: Cli.Args.text({ name: 'member' }).pipe(Cli.Args.withDescription('Member to pin')), - checkout: Cli.Options.text('checkout').pipe( - Cli.Options.withAlias('c'), - Cli.Options.withDescription('Ref to switch to (branch, tag, or commit SHA)'), - Cli.Options.optional, + member: Cli.Argument.string('member').pipe(Cli.Argument.withDescription('Member to pin')), + checkout: Cli.Flag.string('checkout').pipe( + Cli.Flag.withAlias('c'), + Cli.Flag.withDescription('Ref to switch to (branch, tag, or commit SHA)'), + Cli.Flag.optional, ), - dryRun: Cli.Options.boolean('dry-run').pipe( - Cli.Options.withDescription('Show what would be changed without making changes'), - Cli.Options.withDefault(false), + dryRun: Cli.Flag.boolean('dry-run').pipe( + Cli.Flag.withDescription('Show what would be changed without making changes'), + Cli.Flag.withDefault(false), ), output: outputOption, }, @@ -530,7 +530,7 @@ const shortenPath = (path: string): string => { export const unpinCommand = Cli.Command.make( 'unpin', { - member: Cli.Args.text({ name: 'member' }).pipe(Cli.Args.withDescription('Member to unpin')), + member: Cli.Argument.string('member').pipe(Cli.Argument.withDescription('Member to unpin')), output: outputOption, }, ({ member, output }) => diff --git a/packages/@overeng/megarepo/src/cli/commands/root.ts b/packages/@overeng/megarepo/src/cli/commands/root.ts index 2093b55960..af34393478 100644 --- a/packages/@overeng/megarepo/src/cli/commands/root.ts +++ b/packages/@overeng/megarepo/src/cli/commands/root.ts @@ -4,7 +4,7 @@ * Find and print the megarepo root directory. */ -import * as Cli from '@effect/cli' +import * as Cli from 'effect/unstable/cli' import { Effect, Option } from 'effect' import React from 'react' diff --git a/packages/@overeng/megarepo/src/cli/commands/status.ts b/packages/@overeng/megarepo/src/cli/commands/status.ts index 156615244d..696635bade 100644 --- a/packages/@overeng/megarepo/src/cli/commands/status.ts +++ b/packages/@overeng/megarepo/src/cli/commands/status.ts @@ -4,10 +4,11 @@ * Show workspace status and member states. */ -import * as Cli from '@effect/cli' -import type { CommandExecutor } from '@effect/platform' -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Clock, Effect, Option, type ParseResult } from 'effect' +import * as Cli from 'effect/unstable/cli' import React from 'react' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -285,9 +286,9 @@ export const statusCommand = Cli.Command.make( 'status', { output: outputOption, - all: Cli.Options.boolean('all').pipe( - Cli.Options.withDescription('Recursively show status of nested megarepos'), - Cli.Options.withDefault(false), + all: Cli.Flag.boolean('all').pipe( + Cli.Flag.withDescription('Recursively show status of nested megarepos'), + Cli.Flag.withDefault(false), ), }, ({ output, all }) => diff --git a/packages/@overeng/megarepo/src/cli/commands/store/mod.ts b/packages/@overeng/megarepo/src/cli/commands/store/mod.ts index 0456f5368a..b7d32472bf 100644 --- a/packages/@overeng/megarepo/src/cli/commands/store/mod.ts +++ b/packages/@overeng/megarepo/src/cli/commands/store/mod.ts @@ -4,9 +4,10 @@ * Commands for managing the shared git store. */ -import * as Cli from '@effect/cli' -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Clock, Effect, Option, Schedule, Stream } from 'effect' +import * as Cli from 'effect/unstable/cli' import React from 'react' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -1261,18 +1262,18 @@ const storeGcCommand = Cli.Command.make( 'gc', { output: outputOption, - dryRun: Cli.Options.boolean('dry-run').pipe( - Cli.Options.withDescription('Show what would be removed without removing'), - Cli.Options.withDefault(false), + dryRun: Cli.Flag.boolean('dry-run').pipe( + Cli.Flag.withDescription('Show what would be removed without removing'), + Cli.Flag.withDefault(false), ), - force: Cli.Options.boolean('force').pipe( - Cli.Options.withAlias('f'), - Cli.Options.withDescription('Remove dirty worktrees (with uncommitted changes)'), - Cli.Options.withDefault(false), + force: Cli.Flag.boolean('force').pipe( + Cli.Flag.withAlias('f'), + Cli.Flag.withDescription('Remove dirty worktrees (with uncommitted changes)'), + Cli.Flag.withDefault(false), ), - all: Cli.Options.boolean('all').pipe( - Cli.Options.withDescription('Remove all worktrees (not just unused ones)'), - Cli.Options.withDefault(false), + all: Cli.Flag.boolean('all').pipe( + Cli.Flag.withDescription('Remove all worktrees (not just unused ones)'), + Cli.Flag.withDefault(false), ), }, ({ output, dryRun, force, all }) => @@ -1836,8 +1837,8 @@ const storeGcCommand = Cli.Command.make( const storeAddCommand = Cli.Command.make( 'add', { - source: Cli.Args.text({ name: 'source' }).pipe( - Cli.Args.withDescription('Repository source (owner/repo, URL, or owner/repo#ref)'), + source: Cli.Argument.string('source').pipe( + Cli.Argument.withDescription('Repository source (owner/repo, URL, or owner/repo#ref)'), ), output: outputOption, }, @@ -1996,13 +1997,13 @@ const storeFixCommand = Cli.Command.make( 'fix', { output: outputOption, - member: Cli.Args.text({ name: 'member' }).pipe( - Cli.Args.withDescription('Member to fix (optional, fixes all if omitted)'), - Cli.Args.optional, + member: Cli.Argument.string('member').pipe( + Cli.Argument.withDescription('Member to fix (optional, fixes all if omitted)'), + Cli.Argument.optional, ), - dryRun: Cli.Options.boolean('dry-run').pipe( - Cli.Options.withDescription('Show what would be fixed without making changes'), - Cli.Options.withDefault(false), + dryRun: Cli.Flag.boolean('dry-run').pipe( + Cli.Flag.withDescription('Show what would be fixed without making changes'), + Cli.Flag.withDefault(false), ), }, ({ output, member, dryRun }) => @@ -2119,26 +2120,26 @@ const storeFixCommand = Cli.Command.make( const storeWorktreeNewCommand = Cli.Command.make( 'new', { - repo: Cli.Args.text({ name: 'repo' }).pipe( - Cli.Args.withDescription('Repository (owner/repo, URL, or store-relative path)'), + repo: Cli.Argument.string('repo').pipe( + Cli.Argument.withDescription('Repository (owner/repo, URL, or store-relative path)'), ), - ref: Cli.Options.text('ref').pipe( - Cli.Options.withDescription('Branch or tag name to check out'), - Cli.Options.optional, + ref: Cli.Flag.string('ref').pipe( + Cli.Flag.withDescription('Branch or tag name to check out'), + Cli.Flag.optional, ), - base: Cli.Options.text('base').pipe( - Cli.Options.withDescription('Base ref for creating a new branch (used with --ref)'), - Cli.Options.optional, + base: Cli.Flag.string('base').pipe( + Cli.Flag.withDescription('Base ref for creating a new branch (used with --ref)'), + Cli.Flag.optional, ), - commit: Cli.Options.text('commit').pipe( - Cli.Options.withDescription('Commit SHA to check out (detached HEAD)'), - Cli.Options.optional, + commit: Cli.Flag.string('commit').pipe( + Cli.Flag.withDescription('Commit SHA to check out (detached HEAD)'), + Cli.Flag.optional, ), - porcelain: Cli.Options.boolean('porcelain').pipe( - Cli.Options.withDescription( + porcelain: Cli.Flag.boolean('porcelain').pipe( + Cli.Flag.withDescription( 'Output only the worktree path for scripting (e.g. cd $(mr store worktree new ... --porcelain))', ), - Cli.Options.withDefault(false), + Cli.Flag.withDefault(false), ), output: outputOption, }, diff --git a/packages/@overeng/megarepo/src/cli/context.ts b/packages/@overeng/megarepo/src/cli/context.ts index f759d1cbca..84ed25b777 100644 --- a/packages/@overeng/megarepo/src/cli/context.ts +++ b/packages/@overeng/megarepo/src/cli/context.ts @@ -6,9 +6,9 @@ import { resolve } from 'node:path' -import * as Cli from '@effect/cli' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Context, Effect, Layer, Option } from 'effect' +import * as Cli from 'effect/unstable/cli' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -30,7 +30,7 @@ import { InvalidCwdError } from './errors.ts' * - $PWD: logical path (preserves symlinks) - set by the shell * - process.cwd(): physical path (resolves symlinks) */ -export class Cwd extends Context.Tag('megarepo/Cwd')() { +export class Cwd extends Context.Service()('megarepo/Cwd') { static live = Layer.effect( Cwd, Effect.sync(() => { @@ -86,28 +86,28 @@ export class Cwd extends Context.Tag('megarepo/Cwd')() { // ============================================================================= /** Override the working directory */ -export const cwdOption = Cli.Options.text('cwd').pipe( - Cli.Options.withDescription('Override the working directory'), - Cli.Options.optional, +export const cwdOption = Cli.Flag.string('cwd').pipe( + Cli.Flag.withDescription('Override the working directory'), + Cli.Flag.optional, ) /** JSON output format option */ -export const jsonOption = Cli.Options.boolean('json').pipe( - Cli.Options.withDescription('Output in JSON format'), - Cli.Options.withDefault(false), +export const jsonOption = Cli.Flag.boolean('json').pipe( + Cli.Flag.withDescription('Output in JSON format'), + Cli.Flag.withDefault(false), ) /** Stream JSON output as NDJSON (newline-delimited JSON) */ -export const streamOption = Cli.Options.boolean('stream').pipe( - Cli.Options.withDescription('Stream JSON output as NDJSON (requires --json)'), - Cli.Options.withDefault(false), +export const streamOption = Cli.Flag.boolean('stream').pipe( + Cli.Flag.withDescription('Stream JSON output as NDJSON (requires --json)'), + Cli.Flag.withDefault(false), ) /** Verbose output option */ -export const verboseOption = Cli.Options.boolean('verbose').pipe( - Cli.Options.withAlias('v'), - Cli.Options.withDescription('Show detailed output'), - Cli.Options.withDefault(false), +export const verboseOption = Cli.Flag.boolean('verbose').pipe( + Cli.Flag.withAlias('v'), + Cli.Flag.withDescription('Show detailed output'), + Cli.Flag.withDefault(false), ) // ============================================================================= diff --git a/packages/@overeng/megarepo/src/cli/errors.ts b/packages/@overeng/megarepo/src/cli/errors.ts index 3a960b494b..536ee7066b 100644 --- a/packages/@overeng/megarepo/src/cli/errors.ts +++ b/packages/@overeng/megarepo/src/cli/errors.ts @@ -12,7 +12,7 @@ import { Schema } from 'effect' // ============================================================================= /** Error when not in a megarepo directory */ -export class NotInMegarepoError extends Schema.TaggedError()( +export class NotInMegarepoError extends Schema.TaggedErrorClass()( 'NotInMegarepoError', { message: Schema.String, @@ -20,12 +20,12 @@ export class NotInMegarepoError extends Schema.TaggedError() ) {} /** Error when not in a git repository */ -export class NotGitRepoError extends Schema.TaggedError()('NotGitRepoError', { +export class NotGitRepoError extends Schema.TaggedErrorClass()('NotGitRepoError', { message: Schema.String, }) {} /** Error when a member is not found */ -export class MemberNotFoundError extends Schema.TaggedError()( +export class MemberNotFoundError extends Schema.TaggedErrorClass()( 'MemberNotFoundError', { message: Schema.String, @@ -38,7 +38,7 @@ export class MemberNotFoundError extends Schema.TaggedError // ============================================================================= /** Error when lock file is required but missing */ -export class LockFileRequiredError extends Schema.TaggedError()( +export class LockFileRequiredError extends Schema.TaggedErrorClass()( 'LockFileRequiredError', { message: Schema.String, @@ -46,12 +46,12 @@ export class LockFileRequiredError extends Schema.TaggedError()('NoLockFileError', { +export class NoLockFileError extends Schema.TaggedErrorClass()('NoLockFileError', { message: Schema.String, }) {} /** Error when lock file is stale */ -export class StaleLockFileError extends Schema.TaggedError()( +export class StaleLockFileError extends Schema.TaggedErrorClass()( 'StaleLockFileError', { message: Schema.String, @@ -65,7 +65,7 @@ export class StaleLockFileError extends Schema.TaggedError() // ============================================================================= /** Error when source string is invalid */ -export class InvalidSourceError extends Schema.TaggedError()( +export class InvalidSourceError extends Schema.TaggedErrorClass()( 'InvalidSourceError', { message: Schema.String, @@ -74,7 +74,7 @@ export class InvalidSourceError extends Schema.TaggedError() ) {} /** Error when trying to use local path where remote is required */ -export class CannotUseLocalPathError extends Schema.TaggedError()( +export class CannotUseLocalPathError extends Schema.TaggedErrorClass()( 'CannotUseLocalPathError', { message: Schema.String, @@ -82,7 +82,7 @@ export class CannotUseLocalPathError extends Schema.TaggedError()( +export class CannotGetCloneUrlError extends Schema.TaggedErrorClass()( 'CannotGetCloneUrlError', { message: Schema.String, @@ -94,7 +94,7 @@ export class CannotGetCloneUrlError extends Schema.TaggedError()( +export class MemberNotSyncedError extends Schema.TaggedErrorClass()( 'MemberNotSyncedError', { message: Schema.String, @@ -103,14 +103,14 @@ export class MemberNotSyncedError extends Schema.TaggedError()('SyncFailedError', { +export class SyncFailedError extends Schema.TaggedErrorClass()('SyncFailedError', { message: Schema.String, errorCount: Schema.Number, failedMembers: Schema.Array(Schema.String), }) {} /** Error when invalid options are provided */ -export class InvalidOptionsError extends Schema.TaggedError()( +export class InvalidOptionsError extends Schema.TaggedErrorClass()( 'InvalidOptionsError', { message: Schema.String, @@ -122,7 +122,7 @@ export class InvalidOptionsError extends Schema.TaggedError // ============================================================================= /** Error when --cwd path is invalid (doesn't exist or not a directory) */ -export class InvalidCwdError extends Schema.TaggedError()('InvalidCwdError', { +export class InvalidCwdError extends Schema.TaggedErrorClass()('InvalidCwdError', { message: Schema.String, path: Schema.String, }) {} @@ -132,22 +132,25 @@ export class InvalidCwdError extends Schema.TaggedError()('Inva // ============================================================================= /** Error in add command */ -export class AddCommandError extends Schema.TaggedError()('AddCommandError', { +export class AddCommandError extends Schema.TaggedErrorClass()('AddCommandError', { message: Schema.String, }) {} /** Error in exec command */ -export class ExecCommandError extends Schema.TaggedError()('ExecCommandError', { - message: Schema.String, -}) {} +export class ExecCommandError extends Schema.TaggedErrorClass()( + 'ExecCommandError', + { + message: Schema.String, + }, +) {} /** Error in generate command */ -export class GenerateError extends Schema.TaggedError()('GenerateError', { +export class GenerateError extends Schema.TaggedErrorClass()('GenerateError', { message: Schema.String, }) {} /** Error in store command */ -export class StoreCommandError extends Schema.TaggedError()( +export class StoreCommandError extends Schema.TaggedErrorClass()( 'StoreCommandError', { message: Schema.String, @@ -155,7 +158,7 @@ export class StoreCommandError extends Schema.TaggedError()( ) {} /** Error when megarepo structural checks fail */ -export class CheckCommandError extends Schema.TaggedError()( +export class CheckCommandError extends Schema.TaggedErrorClass()( 'CheckCommandError', { message: Schema.String, diff --git a/packages/@overeng/megarepo/src/cli/mod.ts b/packages/@overeng/megarepo/src/cli/mod.ts index ddf860b13f..da17578d64 100644 --- a/packages/@overeng/megarepo/src/cli/mod.ts +++ b/packages/@overeng/megarepo/src/cli/mod.ts @@ -4,7 +4,7 @@ * Main CLI entry point for the `mr` command. */ -import * as Cli from '@effect/cli' +import * as Cli from 'effect/unstable/cli' import { Option } from 'effect' import { rewriteHelpSubcommand } from '@overeng/utils/node/cli-help-rewrite' diff --git a/packages/@overeng/megarepo/src/cli/pin.integration.test.ts b/packages/@overeng/megarepo/src/cli/pin.integration.test.ts index 0ede891098..2cd22fd95c 100644 --- a/packages/@overeng/megarepo/src/cli/pin.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/pin.integration.test.ts @@ -5,9 +5,9 @@ * These tests use direct function calls instead of CLI subprocess to avoid timeouts. */ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option, Schema } from 'effect' import { expect } from 'vitest' @@ -52,7 +52,7 @@ const createMinimalTestSetup = () => 'test-repo': 'test-owner/test-repo', }, } - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( + const configContent = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( config, ) yield* fs.writeFileString( @@ -102,7 +102,7 @@ describe('mr config pin', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const newConfigContent = yield* Schema.encode( - Schema.parseJson(MegarepoConfig, { space: 2 }), + Schema.fromJsonString(MegarepoConfig, { space: 2 }), )(updatedConfig) yield* fs.writeFileString(configPath, newConfigContent + '\n') @@ -110,7 +110,7 @@ describe('mr config pin', () => { const finalConfig = yield* readConfig(workspacePath) expect(finalConfig.members['test-repo']).toBe('test-owner/test-repo#feature-branch') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -135,7 +135,8 @@ describe('mr config pin', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(config1)) + '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(config1)) + + '\n', ) // Now switch to main @@ -152,7 +153,7 @@ describe('mr config pin', () => { expect(Option.getOrNull(source.ref)).toBe('main') } }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -202,7 +203,7 @@ describe('mr config pin', () => { expect(member?.pinned).toBe(true) } }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -257,7 +258,7 @@ describe('mr config pin', () => { expect(member?.pinned).toBe(true) } }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/cli/renderers/AddOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/AddOutput/schema.ts index cc6ea84ab2..d8446b126c 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/AddOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/AddOutput/schema.ts @@ -34,7 +34,7 @@ export const AddSuccessState = Schema.TaggedStruct('Success', { member: Schema.String, source: Schema.String, synced: Schema.Boolean, - syncStatus: Schema.optional(Schema.Literal('cloned', 'synced', 'error')), + syncStatus: Schema.optional(Schema.Literals(['cloned', 'synced', 'error'])), }) /** @@ -48,7 +48,7 @@ export const AddErrorState = Schema.TaggedStruct('Error', { /** * State for add command. */ -export const AddState = Schema.Union(AddIdleState, AddAddingState, AddSuccessState, AddErrorState) +export const AddState = Schema.Union([AddIdleState, AddAddingState, AddSuccessState, AddErrorState]) /** Inferred type for the add command state (idle, adding, success, or error). */ export type AddState = Schema.Schema.Type @@ -78,16 +78,16 @@ export const isAddAdding = (state: AddState): state is typeof AddAddingState.Typ // ============================================================================= /** Tagged union of actions for the add command (set adding, success, or error). */ -export const AddAction = Schema.Union( +export const AddAction = Schema.Union([ Schema.TaggedStruct('SetAdding', { member: Schema.String, source: Schema.String }), Schema.TaggedStruct('SetSuccess', { member: Schema.String, source: Schema.String, synced: Schema.Boolean, - syncStatus: Schema.optional(Schema.Literal('cloned', 'synced', 'error')), + syncStatus: Schema.optional(Schema.Literals(['cloned', 'synced', 'error'])), }), Schema.TaggedStruct('SetError', { error: Schema.String, message: Schema.String }), -) +]) /** Inferred type for add actions. */ export type AddAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/AddOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/AddOutput/view.tsx index eb9e0f2f61..1256e797f1 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/AddOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/AddOutput/view.tsx @@ -2,7 +2,7 @@ * AddOutput View */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/DepsOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/DepsOutput/schema.ts index 1cd94f1c3f..8af632c545 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/DepsOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/DepsOutput/schema.ts @@ -51,7 +51,7 @@ export const DepsErrorState = Schema.TaggedStruct('Error', { }) /** Tagged union of all deps command output states */ -export const DepsState = Schema.Union(DepsSuccessState, DepsEmptyState, DepsErrorState) +export const DepsState = Schema.Union([DepsSuccessState, DepsEmptyState, DepsErrorState]) /** Inferred type for the deps command state union */ export type DepsState = typeof DepsState.Type @@ -77,13 +77,13 @@ export const isDepsError = (state: DepsState): state is typeof DepsErrorState.Ty // ============================================================================= /** Schema for actions that transition the deps command state */ -export const DepsAction = Schema.Union( +export const DepsAction = Schema.Union([ Schema.TaggedStruct('SetDeps', { members: Schema.Array(DepsMember), }), Schema.TaggedStruct('SetEmpty', {}), Schema.TaggedStruct('SetError', { message: Schema.String }), -) +]) /** Inferred type for deps command actions */ export type DepsAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/DepsOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/DepsOutput/view.tsx index bc7bf7283b..1409e23daa 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/DepsOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/DepsOutput/view.tsx @@ -5,7 +5,7 @@ * Shows upstream members with their downstream dependents indented below. */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/EnvOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/EnvOutput/schema.ts index 7697e96a89..0b341f7e22 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/EnvOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/EnvOutput/schema.ts @@ -22,7 +22,7 @@ export const EnvSuccessState = Schema.TaggedStruct('Success', { /** Global store location */ MEGAREPO_STORE: Schema.String, /** Shell type for output formatting (only used in TTY mode) */ - shell: Schema.optional(Schema.Literal('bash', 'zsh', 'fish')), + shell: Schema.optional(Schema.Literals(['bash', 'zsh', 'fish'])), }) /** @@ -36,7 +36,7 @@ export const EnvErrorState = Schema.TaggedStruct('Error', { /** * State for env command. */ -export const EnvState = Schema.Union(EnvSuccessState, EnvErrorState) +export const EnvState = Schema.Union([EnvSuccessState, EnvErrorState]) /** Inferred type for the env command state (success with env vars or error). */ export type EnvState = Schema.Schema.Type @@ -60,15 +60,15 @@ export const isEnvSuccess = (state: EnvState): state is typeof EnvSuccessState.T /** * Actions for env output. */ -export const EnvAction = Schema.Union( +export const EnvAction = Schema.Union([ /** Set success state */ Schema.TaggedStruct('SetEnv', { MEGAREPO_STORE: Schema.String, - shell: Schema.optional(Schema.Literal('bash', 'zsh', 'fish')), + shell: Schema.optional(Schema.Literals(['bash', 'zsh', 'fish'])), }), /** Set error state */ Schema.TaggedStruct('SetError', { error: Schema.String, message: Schema.String }), -) +]) /** Inferred type for env actions. */ export type EnvAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/EnvOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/EnvOutput/view.tsx index 860439ca19..d5f62adb5c 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/EnvOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/EnvOutput/view.tsx @@ -5,7 +5,7 @@ * TTY mode outputs shell-specific export commands. */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/ExecOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/ExecOutput/schema.ts index f77dc940fe..0f00f1d81b 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/ExecOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/ExecOutput/schema.ts @@ -16,7 +16,7 @@ export const MemberExecStatus = Schema.Struct({ /** Member name */ name: Schema.String, /** Current execution status */ - status: Schema.Literal('pending', 'running', 'success', 'error', 'skipped'), + status: Schema.Literals(['pending', 'running', 'success', 'error', 'skipped']), /** Exit code (0 = success, non-zero = error) */ exitCode: Schema.optional(Schema.Number), /** Combined stdout output */ @@ -40,7 +40,7 @@ export const ExecRunningState = Schema.TaggedStruct('Running', { /** Command being executed */ command: Schema.String, /** Execution mode */ - mode: Schema.Literal('parallel', 'sequential'), + mode: Schema.Literals(['parallel', 'sequential']), /** Verbose mode enabled */ verbose: Schema.Boolean, /** All member statuses */ @@ -55,7 +55,7 @@ export const ExecCompleteState = Schema.TaggedStruct('Complete', { /** Command that was executed */ command: Schema.String, /** Execution mode used */ - mode: Schema.Literal('parallel', 'sequential'), + mode: Schema.Literals(['parallel', 'sequential']), /** Verbose mode enabled */ verbose: Schema.Boolean, /** All member results */ @@ -76,7 +76,7 @@ export const ExecErrorState = Schema.TaggedStruct('Error', { /** * State for exec command - discriminated by _tag property. */ -export const ExecState = Schema.Union(ExecRunningState, ExecCompleteState, ExecErrorState) +export const ExecState = Schema.Union([ExecRunningState, ExecCompleteState, ExecErrorState]) export type ExecState = typeof ExecState.Type @@ -101,11 +101,11 @@ export const isExecRunning = (state: ExecState): state is typeof ExecRunningStat // ============================================================================= /** Tagged union of actions that can be dispatched to update exec state. */ -export const ExecAction = Schema.Union( +export const ExecAction = Schema.Union([ /** Initialize exec with members */ Schema.TaggedStruct('Start', { command: Schema.String, - mode: Schema.Literal('parallel', 'sequential'), + mode: Schema.Literals(['parallel', 'sequential']), verbose: Schema.Boolean, members: Schema.Array(Schema.String), }), @@ -113,7 +113,7 @@ export const ExecAction = Schema.Union( /** Update a member's status */ Schema.TaggedStruct('UpdateMember', { name: Schema.String, - status: Schema.Literal('pending', 'running', 'success', 'error', 'skipped'), + status: Schema.Literals(['pending', 'running', 'success', 'error', 'skipped']), exitCode: Schema.optional(Schema.Number), stdout: Schema.optional(Schema.String), stderr: Schema.optional(Schema.String), @@ -127,7 +127,7 @@ export const ExecAction = Schema.Union( error: Schema.String, message: Schema.String, }), -) +]) /** Inferred type for exec actions. */ export type ExecAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/ExecOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/ExecOutput/view.tsx index b05ed88a37..5dd25fe99d 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/ExecOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/ExecOutput/view.tsx @@ -5,7 +5,7 @@ * Handles running, complete, and error states. */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/GenerateOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/GenerateOutput/schema.ts index c73aef7ff2..d7259f05d3 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/GenerateOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/GenerateOutput/schema.ts @@ -54,12 +54,12 @@ export const GenerateErrorState = Schema.TaggedStruct('Error', { /** * State for generate command. */ -export const GenerateState = Schema.Union( +export const GenerateState = Schema.Union([ GenerateIdleState, GenerateRunningState, GenerateSuccessState, GenerateErrorState, -) +]) /** Inferred type for the generate command state (idle, running, success, or error). */ export type GenerateState = Schema.Schema.Type @@ -91,12 +91,12 @@ export const isGenerateIdle = (state: GenerateState): state is typeof GenerateId // ============================================================================= /** Tagged union of actions for the generate command (start, progress, success, error). */ -export const GenerateAction = Schema.Union( +export const GenerateAction = Schema.Union([ Schema.TaggedStruct('Start', { generator: Schema.String }), Schema.TaggedStruct('SetProgress', { generator: Schema.String, progress: Schema.String }), Schema.TaggedStruct('SetSuccess', { results: Schema.Array(GenerateResultItem) }), Schema.TaggedStruct('SetError', { error: Schema.String, message: Schema.String }), -) +]) /** Inferred type for generate actions. */ export type GenerateAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/GenerateOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/GenerateOutput/view.tsx index 633cb8b2d0..14de858310 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/GenerateOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/GenerateOutput/view.tsx @@ -2,7 +2,7 @@ * GenerateOutput View */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols, type Symbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/InitOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/InitOutput/schema.ts index f4fe9ed05e..27c827e3cf 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/InitOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/InitOutput/schema.ts @@ -38,7 +38,7 @@ export const InitErrorState = Schema.TaggedStruct('Error', { /** * State for init command. */ -export const InitState = Schema.Union(InitSuccessState, InitAlreadyState, InitErrorState) +export const InitState = Schema.Union([InitSuccessState, InitAlreadyState, InitErrorState]) /** Inferred type for the init command state (success, already initialized, or error). */ export type InitState = Schema.Schema.Type @@ -64,11 +64,11 @@ export const isInitAlready = (state: InitState): state is typeof InitAlreadyStat // ============================================================================= /** Tagged union of actions for the init command. */ -export const InitAction = Schema.Union( +export const InitAction = Schema.Union([ Schema.TaggedStruct('SetInitialized', { path: Schema.String }), Schema.TaggedStruct('SetAlreadyInitialized', { path: Schema.String }), Schema.TaggedStruct('SetError', { error: Schema.String, message: Schema.String }), -) +]) /** Inferred type for init actions. */ export type InitAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/InitOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/InitOutput/view.tsx index 9c92d07282..683229754e 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/InitOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/InitOutput/view.tsx @@ -4,7 +4,7 @@ import path from 'node:path' -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/LsOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/LsOutput/schema.ts index 35a4456615..b128e5a378 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/LsOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/LsOutput/schema.ts @@ -21,7 +21,7 @@ export const MemberOwnerNested = Schema.TaggedStruct('Nested', { }) /** Discriminated union for member ownership */ -export const MemberOwner = Schema.Union(MemberOwnerRoot, MemberOwnerNested) +export const MemberOwner = Schema.Union([MemberOwnerRoot, MemberOwnerNested]) /** Inferred type representing whether a member belongs to the root or a nested megarepo. */ export type MemberOwner = Schema.Schema.Type @@ -78,7 +78,7 @@ export const LsErrorState = Schema.TaggedStruct('Error', { * Success JSON: { "_tag": "Success", "members": [...] } * Error JSON: { "_tag": "Error", "error": "...", "message": "..." } */ -export const LsState = Schema.Union(LsSuccessState, LsErrorState) +export const LsState = Schema.Union([LsSuccessState, LsErrorState]) export type LsState = typeof LsState.Type @@ -101,7 +101,7 @@ export const isLsSuccess = (state: LsState): state is typeof LsSuccessState.Type /** * Actions for ls output. */ -export const LsAction = Schema.Union( +export const LsAction = Schema.Union([ Schema.TaggedStruct('SetMembers', { members: Schema.Array(MemberInfo), all: Schema.Boolean, @@ -110,7 +110,7 @@ export const LsAction = Schema.Union( currentMemberPath: Schema.optional(Schema.Array(Schema.String)), }), Schema.TaggedStruct('SetError', { error: Schema.String, message: Schema.String }), -) +]) /** Inferred type for ls actions. */ export type LsAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/LsOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/LsOutput/view.tsx index 03f668a5c4..9da2ee9065 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/LsOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/LsOutput/view.tsx @@ -6,7 +6,7 @@ * Supports hierarchical display when --all is used. */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols, unicodeSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/PinOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/PinOutput/schema.ts index 59f3a754f1..7c8b1b35f0 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/PinOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/PinOutput/schema.ts @@ -29,7 +29,7 @@ export const PinCheckingState = Schema.TaggedStruct('Checking', { */ export const PinSuccessState = Schema.TaggedStruct('Success', { member: Schema.String, - action: Schema.Literal('pin', 'unpin'), + action: Schema.Literals(['pin', 'unpin']), ref: Schema.optional(Schema.String), commit: Schema.optional(Schema.String), }) @@ -39,7 +39,7 @@ export const PinSuccessState = Schema.TaggedStruct('Success', { */ export const PinAlreadyState = Schema.TaggedStruct('Already', { member: Schema.String, - action: Schema.Literal('pin', 'unpin'), + action: Schema.Literals(['pin', 'unpin']), commit: Schema.optional(Schema.String), }) @@ -48,7 +48,7 @@ export const PinAlreadyState = Schema.TaggedStruct('Already', { */ export const PinDryRunState = Schema.TaggedStruct('DryRun', { member: Schema.String, - action: Schema.Literal('pin', 'unpin'), + action: Schema.Literals(['pin', 'unpin']), ref: Schema.optional(Schema.String), commit: Schema.optional(Schema.String), currentSource: Schema.optional(Schema.String), @@ -65,7 +65,7 @@ export const PinDryRunState = Schema.TaggedStruct('DryRun', { * Warning state - for non-fatal issues */ export const PinWarningState = Schema.TaggedStruct('Warning', { - warning: Schema.Literal('worktree_not_available', 'member_removed_from_config'), + warning: Schema.Literals(['worktree_not_available', 'member_removed_from_config']), member: Schema.optional(Schema.String), message: Schema.optional(Schema.String), }) @@ -81,7 +81,7 @@ export const PinErrorState = Schema.TaggedStruct('Error', { /** * State for pin/unpin commands. */ -export const PinState = Schema.Union( +export const PinState = Schema.Union([ PinIdleState, PinCheckingState, PinSuccessState, @@ -89,7 +89,7 @@ export const PinState = Schema.Union( PinDryRunState, PinWarningState, PinErrorState, -) +]) /** Inferred type for the pin/unpin command state. */ export type PinState = Schema.Schema.Type @@ -123,22 +123,22 @@ export const isPinWarning = (state: PinState): state is typeof PinWarningState.T // ============================================================================= /** Tagged union of actions for the pin/unpin commands. */ -export const PinAction = Schema.Union( +export const PinAction = Schema.Union([ Schema.TaggedStruct('SetChecking', { member: Schema.String }), Schema.TaggedStruct('SetSuccess', { member: Schema.String, - action: Schema.Literal('pin', 'unpin'), + action: Schema.Literals(['pin', 'unpin']), ref: Schema.optional(Schema.String), commit: Schema.optional(Schema.String), }), Schema.TaggedStruct('SetAlready', { member: Schema.String, - action: Schema.Literal('pin', 'unpin'), + action: Schema.Literals(['pin', 'unpin']), commit: Schema.optional(Schema.String), }), Schema.TaggedStruct('SetDryRun', { member: Schema.String, - action: Schema.Literal('pin', 'unpin'), + action: Schema.Literals(['pin', 'unpin']), ref: Schema.optional(Schema.String), commit: Schema.optional(Schema.String), currentSource: Schema.optional(Schema.String), @@ -151,12 +151,12 @@ export const PinAction = Schema.Union( worktreeNotAvailable: Schema.optional(Schema.Boolean), }), Schema.TaggedStruct('SetWarning', { - warning: Schema.Literal('worktree_not_available', 'member_removed_from_config'), + warning: Schema.Literals(['worktree_not_available', 'member_removed_from_config']), member: Schema.optional(Schema.String), message: Schema.optional(Schema.String), }), Schema.TaggedStruct('SetError', { error: Schema.String, message: Schema.String }), -) +]) /** Inferred type for pin/unpin actions. */ export type PinAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/PinOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/PinOutput/view.tsx index 811fab0930..bd7bc5a830 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/PinOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/PinOutput/view.tsx @@ -2,7 +2,7 @@ * PinOutput View */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/PushRefsOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/PushRefsOutput/schema.ts index 50d4a91e35..ee0e619890 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/PushRefsOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/PushRefsOutput/schema.ts @@ -53,13 +53,13 @@ export const PushRefsErrorState = Schema.TaggedStruct('Error', { }) /** Union of all possible push-refs TUI states */ -export const PushRefsState = Schema.Union( +export const PushRefsState = Schema.Union([ PushRefsIdleState, PushRefsScanningState, PushRefsAlignedState, PushRefsResultState, PushRefsErrorState, -) +]) export type PushRefsState = typeof PushRefsState.Type @@ -68,7 +68,7 @@ export type PushRefsState = typeof PushRefsState.Type // ============================================================================= /** Union of all actions dispatched to the push-refs reducer */ -export const PushRefsAction = Schema.Union( +export const PushRefsAction = Schema.Union([ Schema.TaggedStruct('SetScanning', {}), Schema.TaggedStruct('SetAligned', {}), Schema.TaggedStruct('SetResult', { @@ -80,7 +80,7 @@ export const PushRefsAction = Schema.Union( error: Schema.String, message: Schema.String, }), -) +]) export type PushRefsAction = typeof PushRefsAction.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/PushRefsOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/PushRefsOutput/view.tsx index e5efcde7e1..ec21c14795 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/PushRefsOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/PushRefsOutput/view.tsx @@ -4,7 +4,7 @@ * Renders results of propagating member refs from parent to nested megarepo configs. */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/RootOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/RootOutput/schema.ts index db4e5607fc..2b6903d701 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/RootOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/RootOutput/schema.ts @@ -32,7 +32,7 @@ export const RootErrorState = Schema.TaggedStruct('Error', { /** * State for root command - discriminated by _tag. */ -export const RootState = Schema.Union(RootSuccessState, RootErrorState) +export const RootState = Schema.Union([RootSuccessState, RootErrorState]) /** Inferred type for root command state (success with path or error). */ export type RootState = Schema.Schema.Type @@ -54,13 +54,13 @@ export const isRootSuccess = (state: RootState): state is typeof RootSuccessStat // ============================================================================= /** Tagged union of actions for the root command (set success or error). */ -export const RootAction = Schema.Union( +export const RootAction = Schema.Union([ Schema.TaggedStruct('SetSuccess', { root: Schema.String, name: Schema.String, }), Schema.TaggedStruct('SetError', { error: Schema.String, message: Schema.String }), -) +]) /** Inferred type for root actions. */ export type RootAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/RootOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/RootOutput/view.tsx index 0a371c1c9a..ef752ef056 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/RootOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/RootOutput/view.tsx @@ -2,7 +2,7 @@ * RootOutput View */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/StatusOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/StatusOutput/view.tsx index da6f3060b9..e4d10b54f1 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/StatusOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/StatusOutput/view.tsx @@ -7,7 +7,7 @@ * Uses megarepo's design system components for consistent styling. */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, unicodeSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/StoreOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/StoreOutput/schema.ts index 2435bc20d8..254696c8a9 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/StoreOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/StoreOutput/schema.ts @@ -22,7 +22,7 @@ export type StoreRepo = Schema.Schema.Type /** Schema for the result of fetching a single repository in the store. */ export const StoreFetchResult = Schema.Struct({ path: Schema.String, - status: Schema.Literal('fetched', 'error'), + status: Schema.Literals(['fetched', 'error']), message: Schema.optional(Schema.String), }) @@ -38,7 +38,7 @@ export type StoreFetchResult = Schema.Schema.Type * `reaped` (an archive past retention hard-deleted), and `kept` (a cold named * worktree deliberately left in place, e.g. live/not-stale/lossless/grace). */ -export const StoreGcResultStatus = Schema.Literal( +export const StoreGcResultStatus = Schema.Literals([ 'removed', 'skipped_dirty', 'skipped_in_use', @@ -46,7 +46,7 @@ export const StoreGcResultStatus = Schema.Literal( 'archived', 'reaped', 'kept', -) +]) /** Inferred type for a GC result status. */ export type StoreGcResultStatus = Schema.Schema.Type @@ -55,7 +55,7 @@ export type StoreGcResultStatus = Schema.Schema.Type export const StoreGcResult = Schema.Struct({ repo: Schema.String, ref: Schema.String, - refType: Schema.Literal('heads', 'tags', 'commits'), + refType: Schema.Literals(['heads', 'tags', 'commits']), path: Schema.String, status: StoreGcResultStatus, message: Schema.optional(Schema.String), @@ -78,15 +78,15 @@ export type StoreGcResult = Schema.Schema.Type /** Schema for a health issue detected on a worktree (dirty, unpushed, orphaned, etc.). */ export const StoreWorktreeIssue = Schema.Struct({ - type: Schema.Literal( + type: Schema.Literals([ 'dirty', 'unpushed', 'ref_mismatch', 'missing_bare', 'broken_worktree', 'orphaned', - ), - severity: Schema.Literal('error', 'warning', 'info'), + ]), + severity: Schema.Literals(['error', 'warning', 'info']), message: Schema.String, }) @@ -97,7 +97,7 @@ export type StoreWorktreeIssue = Schema.Schema.Type export const StoreWorktreeStatus = Schema.Struct({ repo: Schema.String, ref: Schema.String, - refType: Schema.Literal('heads', 'tags', 'commits'), + refType: Schema.Literals(['heads', 'tags', 'commits']), path: Schema.String, issues: Schema.Array(StoreWorktreeIssue), }) @@ -107,7 +107,7 @@ export type StoreWorktreeStatus = Schema.Schema.Type /** Schema for warnings shown before garbage collection (e.g., not in megarepo). */ export const StoreGcWarning = Schema.Struct({ - type: Schema.Literal('not_in_megarepo', 'only_current_megarepo', 'custom'), + type: Schema.Literals(['not_in_megarepo', 'only_current_megarepo', 'custom']), message: Schema.optional(Schema.String), }) @@ -169,7 +169,7 @@ export const StoreGcState = Schema.TaggedStruct('Gc', { * Add state - add to store */ export const StoreAddState = Schema.TaggedStruct('Add', { - status: Schema.Literal('added', 'already_exists', 'created'), + status: Schema.Literals(['added', 'already_exists', 'created']), source: Schema.String, ref: Schema.String, commit: Schema.optional(Schema.String), @@ -180,7 +180,7 @@ export const StoreAddState = Schema.TaggedStruct('Add', { export const StoreFixResult = Schema.Struct({ memberName: Schema.String, issueType: Schema.String, - status: Schema.Literal('fixed', 'skipped', 'error'), + status: Schema.Literals(['fixed', 'skipped', 'error']), message: Schema.String, }) @@ -226,7 +226,7 @@ export const StoreInterruptedState = Schema.TaggedStruct('Interrupted', {}) /** * State for all store commands - discriminated by _tag property. */ -export const StoreState = Schema.Union( +export const StoreState = Schema.Union([ StoreLsState, StoreStatusState, StoreFetchState, @@ -236,7 +236,7 @@ export const StoreState = Schema.Union( StoreFixState, StoreErrorState, StoreInterruptedState, -) +]) export type StoreState = typeof StoreState.Type @@ -283,7 +283,7 @@ export const isStoreFix = (state: StoreState): state is typeof StoreFixState.Typ /** * Actions for store output. */ -export const StoreAction = Schema.Union( +export const StoreAction = Schema.Union([ Schema.TaggedStruct('SetLs', { basePath: Schema.String, repos: Schema.Array(StoreRepo), @@ -316,7 +316,7 @@ export const StoreAction = Schema.Union( interrupted: Schema.optional(Schema.Boolean), }), Schema.TaggedStruct('SetAdd', { - status: Schema.Literal('added', 'already_exists', 'created'), + status: Schema.Literals(['added', 'already_exists', 'created']), source: Schema.String, ref: Schema.String, commit: Schema.optional(Schema.String), @@ -342,7 +342,7 @@ export const StoreAction = Schema.Union( source: Schema.optional(Schema.String), }), Schema.TaggedStruct('Interrupted', {}), -) +]) /** Inferred type for store actions. */ export type StoreAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/StoreOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/StoreOutput/view.tsx index ab1b9e3ab7..76839ab2e9 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/StoreOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/StoreOutput/view.tsx @@ -5,7 +5,7 @@ * Handles ls, status, fetch, gc, add, and error states. */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, unicodeSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/megarepo/src/cli/renderers/SyncOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/SyncOutput/schema.ts index 85807278fe..24fde1da2e 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/SyncOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/SyncOutput/schema.ts @@ -20,7 +20,7 @@ import { /** Schema for a store hygiene issue shown in pre-flight failure output */ export const PreflightIssue = Schema.Struct({ - severity: Schema.Literal('error', 'warning', 'info'), + severity: Schema.Literals(['error', 'warning', 'info']), type: Schema.String, memberName: Schema.String, message: Schema.String, @@ -70,18 +70,18 @@ export const LockSharedSourceUpdate = Schema.TaggedStruct('SharedSourceUpdate', export type LockSharedSourceUpdate = Schema.Schema.Type /** Union of rev, ref, and scheme update types (used in lock file sync results) */ -export const LockFileUpdate = Schema.Union(LockRevUpdate, LockRefUpdate, LockSchemeUpdate) +export const LockFileUpdate = Schema.Union([LockRevUpdate, LockRefUpdate, LockSchemeUpdate]) /** Inferred type for a lock file update (rev or ref) */ export type LockFileUpdate = Schema.Schema.Type /** Union of all lock sync update types */ -export const LockSyncUpdate = Schema.Union(LockRevUpdate, LockRefUpdate, LockSharedSourceUpdate) +export const LockSyncUpdate = Schema.Union([LockRevUpdate, LockRefUpdate, LockSharedSourceUpdate]) /** Inferred type for any lock sync update (rev, ref, or shared source) */ export type LockSyncUpdate = Schema.Schema.Type /** Schema for lock file sync result */ export const LockFileSyncResult = Schema.Struct({ - type: Schema.Literal('flake.lock', 'devenv.lock', 'megarepo.lock', 'flake.nix', 'devenv.yaml'), + type: Schema.Literals(['flake.lock', 'devenv.lock', 'megarepo.lock', 'flake.nix', 'devenv.yaml']), updatedInputs: Schema.Array(LockFileUpdate), }) /** Inferred type for a lock file sync result. */ @@ -102,13 +102,13 @@ export type MemberLockSyncResult = Schema.Schema.Type @@ -119,7 +119,7 @@ export type SyncOutcome = Schema.Schema.Type /** Schema for a log entry displayed during TTY progress output. */ export const SyncLogEntry = Schema.Struct({ id: Schema.String, - type: Schema.Literal('info', 'warn', 'error'), + type: Schema.Literals(['info', 'warn', 'error']), message: Schema.String, }) /** Inferred type for a sync log entry. */ @@ -211,7 +211,7 @@ export type SyncState = Schema.Schema.Type let logIdCounter = 0 /** Tagged union of actions for progressing through a sync operation. */ -export const SyncAction = Schema.Union( +export const SyncAction = Schema.Union([ /** Replace entire state */ Schema.TaggedStruct('SetState', { state: SyncState }), @@ -230,7 +230,7 @@ export const SyncAction = Schema.Union( /** Add a log entry */ Schema.TaggedStruct('AddLog', { - type: Schema.Literal('info', 'warn', 'error'), + type: Schema.Literals(['info', 'warn', 'error']), message: Schema.String, }), @@ -257,7 +257,7 @@ export const SyncAction = Schema.Union( Schema.TaggedStruct('PreflightFailed', { issues: Schema.Array(PreflightIssue), }), -) +]) /** Inferred type for sync actions. */ export type SyncAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/renderers/SyncOutput/view.tsx b/packages/@overeng/megarepo/src/cli/renderers/SyncOutput/view.tsx index 06967bb356..4a79629574 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/SyncOutput/view.tsx +++ b/packages/@overeng/megarepo/src/cli/renderers/SyncOutput/view.tsx @@ -7,7 +7,7 @@ * Uses megarepo's design system components for consistent styling. */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React, { type ReactNode } from 'react' import { useMemo } from 'react' diff --git a/packages/@overeng/megarepo/src/cli/status.integration.test.ts b/packages/@overeng/megarepo/src/cli/status.integration.test.ts index 567ae97dcb..92abdde009 100644 --- a/packages/@overeng/megarepo/src/cli/status.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/status.integration.test.ts @@ -7,11 +7,11 @@ * - commitDrift field */ -import * as Cli from '@effect/cli' -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' +import { FileSystem } from 'effect/FileSystem' import { Effect, Exit, Schema } from 'effect' +import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -53,7 +53,7 @@ const runStatusCommand = ({ // Parse JSON output let status: StatusState | undefined if (stdout.trim() !== '') { - status = yield* Schema.decodeUnknown(Schema.parseJson(StatusState))(stdout) + status = yield* Schema.decodeUnknown(Schema.fromJsonString(StatusState))(stdout) } return { @@ -89,7 +89,7 @@ const createTestWorkspace = (args: { const config: MegarepoConfig = { members: args.members, } - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( + const configContent = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( config, ) yield* fs.writeFileString( @@ -173,7 +173,7 @@ describe('mr status --output json', () => { expect(status!.syncNeeded).toBe(false) expect(status!.syncReasons).toEqual([]) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -202,7 +202,7 @@ describe('mr status --output json', () => { expect(status!.syncNeeded).toBe(true) expect(status!.syncReasons).toContain("Member 'effect' symlink missing") }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -224,7 +224,7 @@ describe('mr status --output json', () => { expect(status!.syncNeeded).toBe(true) expect(status!.syncReasons).toContain('Lock file missing') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 20000 }, @@ -257,7 +257,7 @@ describe('mr status --output json', () => { expect(status!.syncNeeded).toBe(true) expect(status!.syncReasons).toContain("Member 'another-lib' not in lock file") }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -294,7 +294,7 @@ describe('mr status --output json', () => { expect(member).toBeDefined() expect(member!.symlinkExists).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -323,7 +323,7 @@ describe('mr status --output json', () => { expect(member).toBeDefined() expect(member!.symlinkExists).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -373,7 +373,7 @@ describe('mr status --output json', () => { expect(member!.commitDrift!.localCommit).toBe(actualCommit) expect(member!.commitDrift!.lockedCommit).toBe(lockedCommit) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -414,7 +414,7 @@ describe('mr status --output json', () => { expect(member).toBeDefined() expect(member!.commitDrift).toBeUndefined() }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -447,7 +447,7 @@ describe('mr status --output json', () => { expect(member!.isLocal).toBe(true) expect(member!.commitDrift).toBeUndefined() }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -510,7 +510,7 @@ describe('mr status --output json', () => { expect(status!.syncNeeded).toBe(true) expect(status!.syncReasons).toContain("Member 'remote-lib' symlink missing") }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -534,7 +534,7 @@ describe('mr status --output json', () => { const writeConfig = (workspacePath: AbsoluteDirPath, members: Record) => Effect.gen(function* () { const configContent = yield* Schema.encode( - Schema.parseJson(MegarepoConfig, { space: 2 }), + Schema.fromJsonString(MegarepoConfig, { space: 2 }), )({ members }) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile('megarepo.json')), @@ -576,7 +576,7 @@ describe('mr status --output json', () => { expect(cycleClosingMember?.isMegarepo).toBe(true) expect(cycleClosingMember?.nestedMembers).toEqual([]) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/cli/store-gc-cold.integration.test.ts b/packages/@overeng/megarepo/src/cli/store-gc-cold.integration.test.ts index 98b283b8db..f9b721cbd0 100644 --- a/packages/@overeng/megarepo/src/cli/store-gc-cold.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/store-gc-cold.integration.test.ts @@ -26,11 +26,12 @@ * (`status`/`reason` in the JSON document and the on-disk effect). */ -import * as Cli from '@effect/cli' -import { Command, FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' import { Clock, Effect, Exit, Layer, Schema } from 'effect' +import * as Cli from 'effect/unstable/cli' import { expect, vi } from 'vitest' import { EffectPath, type AbsoluteDirPath, type RelativeDirPath } from '@overeng/effect-path' @@ -99,7 +100,7 @@ const StoreGcJsonOutput = Schema.Struct({ }), ), }) -const decodeGc = Schema.decodeUnknownSync(Schema.parseJson(StoreGcJsonOutput)) +const decodeGc = Schema.decodeUnknownSync(Schema.fromJsonString(StoreGcJsonOutput)) type GcResult = Schema.Schema.Type['results'][number] const findByRef = (results: ReadonlyArray, ref: string) => @@ -255,7 +256,7 @@ describe('mr store gc — cold named-branch reclamation', () => { yield* git(bareRepoPath, 'worktree', 'add', reAddPath, 'feature/merged') expect(yield* fs.exists(reAddPath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -292,7 +293,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(yield* fs.exists(worktreePath)).toBe(true) expect(yield* Git.refExists({ repoPath: bareRepoPath, ref: 'refs/heads/trunk' })).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -335,7 +336,7 @@ describe('mr store gc — cold named-branch reclamation', () => { ), ).toBe('uncommitted changes\n') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -375,7 +376,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.reason).toBe('unrecoverable-local-work') expect(yield* fs.exists(worktreePath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -414,7 +415,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.reason).toBe('unrecoverable-local-work') expect(yield* fs.exists(worktreePath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -442,7 +443,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.status).toBe('kept') expect(result?.reason).toBe('not-stale') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -471,7 +472,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.status).toBe('kept') expect(result?.reason).toBe('not-stale') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -510,7 +511,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.status).toBe('kept') expect(result?.reason).toBe('post-merge-grace') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -541,7 +542,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.status).toBe('kept') expect(result?.reason).toBe('absence-grace') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -601,7 +602,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(yield* fs.exists(livePath)).toBe(true) expect(yield* fs.exists(deadPath)).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -659,7 +660,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(findByRef(results, 'feature/new')?.reason).toBe('live') expect(yield* fs.exists(newPath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -713,7 +714,7 @@ describe('mr store gc — cold named-branch reclamation', () => { // live (last-known path retained) — NOT archived. expect(yield* fs.exists(protectedPath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -752,7 +753,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.actualHeadBranch).toBe('feature/other') expect(yield* fs.exists(worktreePath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -811,7 +812,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(readme).toContain('ref_mismatch_clean') expect(readme).toContain('actualHeadBranch=feature/other') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -849,7 +850,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.recoverPath).toBeUndefined() expect(yield* fs.exists(worktreePath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -885,7 +886,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.reason).toBe('fetch-failed') expect(yield* fs.exists(worktreePath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -946,7 +947,7 @@ describe('mr store gc — cold named-branch reclamation', () => { }), ).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -998,7 +999,7 @@ describe('mr store gc — cold named-branch reclamation', () => { results.some((r) => r.status === 'reaped' && r.ref === 'feature/fresh-archive'), ).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1054,7 +1055,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.reason).toBe('live') expect(yield* fs.exists(archivePath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1121,7 +1122,7 @@ describe('mr store gc — cold named-branch reclamation', () => { }), ).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1159,7 +1160,7 @@ describe('mr store gc — cold named-branch reclamation', () => { yield* Git.refExists({ repoPath: bareRepoPath, ref: 'refs/heads/feature/closed' }), ).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1201,7 +1202,7 @@ describe('mr store gc — cold named-branch reclamation', () => { yield* Git.refExists({ repoPath: bareRepoPath, ref: 'refs/heads/feature/merged' }), ).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1244,7 +1245,7 @@ describe('mr store gc — cold named-branch reclamation', () => { ) expect(yield* fs.exists(archivePath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1313,7 +1314,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.reason).toBe('absence-grace') expect(yield* fs.exists(worktreePath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1346,7 +1347,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(result?.reason).toBeUndefined() expect(yield* fs.exists(worktreePath)).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1381,7 +1382,7 @@ describe('mr store gc — cold named-branch reclamation', () => { // archive based on a clock the dry-run started. expect(yield* fs.exists(ledgerPath)).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1423,7 +1424,7 @@ describe('mr store gc — cold named-branch reclamation', () => { expect(reaped?.status).toBe('reaped') expect(yield* fs.exists(archivePath)).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/cli/store-gc-otel.integration.test.ts b/packages/@overeng/megarepo/src/cli/store-gc-otel.integration.test.ts index a9f74f6333..96b1c3ab9d 100644 --- a/packages/@overeng/megarepo/src/cli/store-gc-otel.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/store-gc-otel.integration.test.ts @@ -19,11 +19,12 @@ * clock (the old clock-coupled sampler would hot-loop and hang). */ -import * as Cli from '@effect/cli' -import { Command, FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' import { Clock, Effect, Layer, Option, Ref, Schema } from 'effect' +import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' import { EffectPath, type AbsoluteDirPath, type RelativeDirPath } from '@overeng/effect-path' @@ -94,7 +95,7 @@ const StoreGcJsonOutput = Schema.Struct({ }), ), }) -const decodeGc = Schema.decodeUnknownSync(Schema.parseJson(StoreGcJsonOutput)) +const decodeGc = Schema.decodeUnknownSync(Schema.fromJsonString(StoreGcJsonOutput)) type GcResults = (typeof StoreGcJsonOutput.Type)['results'] /** @@ -204,7 +205,7 @@ describe('mr store gc — OTEL instrumentation contract', () => { name: 'git/cmd', attrs: { 'git.subcommand': 'fetch', 'git.timeout_ms': attr.int(600_000) }, }) - }).pipe(Effect.provide(Layer.mergeAll(OteliteTestHarness.Default, NodeContext.layer))), + }).pipe(Effect.provide(Layer.mergeAll(OteliteTestHarness.Default, NodeServices.layer))), 30_000, ) @@ -290,7 +291,7 @@ describe('mr store gc — OTEL instrumentation contract', () => { value: metricValue.predicate('rss > 0', (rss) => rss > 0), attrs: { 'megarepo.store.gc.repo_concurrency': telemetryAttr.present() }, }) - }).pipe(Effect.provide(Layer.mergeAll(OteliteTestHarness.Default, NodeContext.layer))), + }).pipe(Effect.provide(Layer.mergeAll(OteliteTestHarness.Default, NodeServices.layer))), 60_000, ) }) diff --git a/packages/@overeng/megarepo/src/cli/store.integration.test.ts b/packages/@overeng/megarepo/src/cli/store.integration.test.ts index 75be7e2eae..9591cf84b3 100644 --- a/packages/@overeng/megarepo/src/cli/store.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/store.integration.test.ts @@ -4,11 +4,11 @@ * Tests the store GC, ls, and fetch commands with realistic store fixtures. */ -import * as Cli from '@effect/cli' -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' +import { FileSystem } from 'effect/FileSystem' import { Effect, Exit, Option, Schema } from 'effect' +import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -39,7 +39,7 @@ const StoreGcJsonOutput = Schema.Struct({ ), }) -const decodeStoreGcJsonOutput = Schema.decodeUnknownSync(Schema.parseJson(StoreGcJsonOutput)) +const decodeStoreGcJsonOutput = Schema.decodeUnknownSync(Schema.fromJsonString(StoreGcJsonOutput)) type StoreGcJsonResult = Schema.Schema.Type['results'][number] @@ -129,7 +129,7 @@ describe('mr store gc', () => { expect(repos).toHaveLength(1) expect(repos[0]?.relativePath).toBe('github.com/test-owner/test-repo/') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -167,7 +167,7 @@ describe('mr store gc', () => { const repos = yield* store.listRepos() expect(repos).toHaveLength(1) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -217,7 +217,7 @@ describe('mr store gc', () => { expect(lockFile.members['my-lib']).toBeDefined() expect(lockFile.members['my-lib']!.ref).toBe('main') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -311,7 +311,7 @@ describe('mr store gc', () => { expect(repoBResult?.status).toBe('kept') expect(yield* fs.exists(repoBPath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -373,7 +373,7 @@ describe('mr store gc', () => { expect(commitResult?.status).toBe('skipped_in_use') expect(yield* fs.exists(commitWorktreePath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -435,7 +435,7 @@ describe('mr store gc', () => { ), ).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -506,7 +506,7 @@ describe('store discovery is bounded to the layout', () => { expect(repos.some((p) => p.startsWith('stray/'))).toBe(false) expect(repos).toHaveLength(2) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -550,7 +550,7 @@ describe('store discovery is bounded to the layout', () => { expect(featureResults[0]?.ref).toBe('feature') expect(results.some((r) => r.ref.includes('node_modules'))).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -593,7 +593,7 @@ describe('store discovery is bounded to the layout', () => { expect(worktrees.every((w) => w.ref.split('/').length <= 8)).toBe(true) expect(worktrees.some((w) => w.ref.includes('d29'))).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -630,7 +630,7 @@ describe('mr store ls', () => { expect(paths).toContain('github.com/owner1/repo-a/') expect(paths).toContain('github.com/owner2/repo-b/') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -652,7 +652,7 @@ describe('mr store ls', () => { const repos = yield* store.listRepos() expect(repos).toHaveLength(0) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -689,7 +689,7 @@ describe('mr store ls', () => { const repos = yield* store.listRepos() expect(repos.map((repo) => repo.relativePath)).toStrictEqual(['localhost/owner/repo/']) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -717,7 +717,7 @@ describe('mr store ls', () => { const repos = yield* store.listRepos() expect(repos.map((repo) => repo.relativePath)).toContain('example.com/refs/project/') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -763,7 +763,7 @@ describe('store worktree paths', () => { }) expect(commitPath).toContain('refs/commits/') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -789,7 +789,7 @@ describe('store worktree paths', () => { }) expect(pathWithSlash).toContain('refs/heads/feature/my-branch/') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -834,7 +834,7 @@ describe('lock file pin/unpin operations', () => { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', ) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -880,7 +880,7 @@ describe('lock file pin/unpin operations', () => { 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', ) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -929,7 +929,7 @@ describe('lock file pin/unpin operations', () => { '2222222222222222222222222222222222222222', ) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -968,7 +968,7 @@ describe('lock file operations', () => { 'abc1234567890abcdef1234567890abcdef1234', ) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1017,7 +1017,7 @@ describe('lock file operations', () => { expect(lockFile.members['lib2']!.pinned).toBe(false) expect(lockFile.members['lib3']!.pinned).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/cli/sync.integration.test.ts b/packages/@overeng/megarepo/src/cli/sync.integration.test.ts index 5e548e5053..b7482bfe1d 100644 --- a/packages/@overeng/megarepo/src/cli/sync.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/sync.integration.test.ts @@ -1,10 +1,10 @@ import { pathToFileURL } from 'node:url' -import * as Cli from '@effect/cli' -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' +import { FileSystem } from 'effect/FileSystem' import { Cause, Chunk, Effect, Exit, Option, Schema } from 'effect' +import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -58,7 +58,7 @@ const SyncJsonOutput = Schema.Struct({ ), }) -const decodeSyncJsonOutput = Schema.decodeUnknownSync(Schema.parseJson(SyncJsonOutput)) +const decodeSyncJsonOutput = Schema.decodeUnknownSync(Schema.fromJsonString(SyncJsonOutput)) /** Run an `mr` CLI command and capture output. */ const runMrCommand = ({ @@ -250,7 +250,7 @@ describe('mr apply', () => { // Note: Actually running the sync command would require more setup // (proper CLI runner, etc). This test verifies the workspace fixture works. }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -294,7 +294,7 @@ describe('mr apply', () => { // The link target should be the repo path without trailing slash expect(linkTarget).toBe(repoPaths['repo1']?.slice(0, -1)) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -457,7 +457,7 @@ describe('lock apply mode', () => { // Should not be stale - lock apply mode would succeed expect(result.isStale).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -499,7 +499,7 @@ describe('lock apply mode', () => { expect(result.isStale).toBe(true) expect(result.addedMembers).toContain('lib2') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -545,7 +545,7 @@ describe('lock apply mode', () => { expect(result.isStale).toBe(true) expect(result.removedMembers).toContain('old-lib') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -590,7 +590,7 @@ describe('lock apply mode', () => { const result = checkLockStaleness({ lockFile, configMemberNames }) expect(result.isStale).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -632,9 +632,9 @@ const createNestedMegarepoFixture = () => 'grandchild-lib': grandchildPath, }, } - const childConfigContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - childConfig, - ) + const childConfigContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(childConfig) yield* fs.writeFileString( EffectPath.ops.join(childPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), childConfigContent + '\n', @@ -659,7 +659,7 @@ const createNestedMegarepoFixture = () => }, } const parentConfigContent = yield* Schema.encode( - Schema.parseJson(MegarepoConfig, { space: 2 }), + Schema.fromJsonString(MegarepoConfig, { space: 2 }), )(parentConfig) yield* fs.writeFileString( EffectPath.ops.join(parentPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), @@ -702,12 +702,12 @@ describe('--all sync mode', () => { // Read parent config and verify it points to child const parentConfigContent = yield* fs.readFileString(parentConfigPath) - const parentConfig = yield* Schema.decodeUnknown(Schema.parseJson(MegarepoConfig))( + const parentConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( parentConfigContent, ) expect(parentConfig.members['child-megarepo']).toBe(childPath) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -725,7 +725,7 @@ describe('--all sync mode', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const childConfigContent = yield* fs.readFileString(childConfigPath) - const childConfig = yield* Schema.decodeUnknown(Schema.parseJson(MegarepoConfig))( + const childConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( childConfigContent, ) expect(childConfig.members['grandchild-lib']).toBe(grandchildPath) @@ -737,7 +737,7 @@ describe('--all sync mode', () => { ) expect(yield* fs.exists(grandchildConfigPath)).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -763,7 +763,7 @@ describe('--all nested error reporting', () => { yield* initGitRepo(childPath) yield* fs.writeFileString( EffectPath.ops.join(childPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { bad: 'not-a-valid-source', }, @@ -780,7 +780,7 @@ describe('--all nested error reporting', () => { yield* initGitRepo(parentPath) yield* fs.writeFileString( EffectPath.ops.join(parentPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { child: childPath, }, @@ -806,7 +806,9 @@ describe('--all nested error reporting', () => { syncErrors: Schema.Array(SyncErrorItem), syncTree: MegarepoSyncTree, }) - const out = yield* Schema.decodeUnknown(Schema.parseJson(SyncOutput))(result.stdout.trim()) + const out = yield* Schema.decodeUnknown(Schema.fromJsonString(SyncOutput))( + result.stdout.trim(), + ) // The command should surface nested errors in output (and set process.exitCode via SyncApp) expect(out.syncErrorCount).toBe(1) @@ -830,7 +832,7 @@ describe('--all nested error reporting', () => { expect(nestedResults.some((r) => r.name === 'bad' && r.status === 'error')).toBe(true) } }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -877,7 +879,7 @@ const createDiamondDependencyFixture = () => members: { 'shared-lib': sharedLibPath }, } const childAConfigContent = yield* Schema.encode( - Schema.parseJson(MegarepoConfig, { space: 2 }), + Schema.fromJsonString(MegarepoConfig, { space: 2 }), )(childAConfig) yield* fs.writeFileString( EffectPath.ops.join(childAPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), @@ -896,7 +898,7 @@ const createDiamondDependencyFixture = () => members: { 'shared-lib': sharedLibPath }, } const childBConfigContent = yield* Schema.encode( - Schema.parseJson(MegarepoConfig, { space: 2 }), + Schema.fromJsonString(MegarepoConfig, { space: 2 }), )(childBConfig) yield* fs.writeFileString( EffectPath.ops.join(childBPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), @@ -917,9 +919,9 @@ const createDiamondDependencyFixture = () => 'child-b': childBPath, }, } - const rootConfigContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - rootConfig, - ) + const rootConfigContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(rootConfig) yield* fs.writeFileString( EffectPath.ops.join(rootPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), rootConfigContent + '\n', @@ -952,7 +954,7 @@ describe('--all sync deduplication', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const rootConfigContent = yield* fs.readFileString(rootConfigPath) - const rootConfig = yield* Schema.decodeUnknown(Schema.parseJson(MegarepoConfig))( + const rootConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( rootConfigContent, ) expect(rootConfig.members['child-a']).toBe(childAPath) @@ -964,7 +966,7 @@ describe('--all sync deduplication', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const childAConfigContent = yield* fs.readFileString(childAConfigPath) - const childAConfig = yield* Schema.decodeUnknown(Schema.parseJson(MegarepoConfig))( + const childAConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( childAConfigContent, ) expect(childAConfig.members['shared-lib']).toBe(sharedLibPath) @@ -974,7 +976,7 @@ describe('--all sync deduplication', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const childBConfigContent = yield* fs.readFileString(childBConfigPath) - const childBConfig = yield* Schema.decodeUnknown(Schema.parseJson(MegarepoConfig))( + const childBConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( childBConfigContent, ) expect(childBConfig.members['shared-lib']).toBe(sharedLibPath) @@ -982,7 +984,7 @@ describe('--all sync deduplication', () => { // Both children reference the SAME path expect(childAConfig.members['shared-lib']).toBe(childBConfig.members['shared-lib']) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1117,7 +1119,7 @@ const createNestedWorkspaceFixtureFromStore = (store: StoreFixtureResult) => yield* initGitRepo(childPath) yield* fs.writeFileString( EffectPath.ops.join(childPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { shared: 'https://example.com/acme/shared#main', }, @@ -1144,7 +1146,7 @@ const createNestedWorkspaceFixtureFromStore = (store: StoreFixtureResult) => yield* initGitRepo(parentPath) yield* fs.writeFileString( EffectPath.ops.join(parentPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { shared: 'https://example.com/acme/shared#main', child: childPath, @@ -1202,7 +1204,7 @@ const createAliasWorkspaceFixture = () => EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const parentConfigContent = yield* fs.readFileString(parentConfigPath) - const parentConfig = yield* Schema.decodeUnknown(Schema.parseJson(MegarepoConfig))( + const parentConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( parentConfigContent, ) const updatedConfig = { @@ -1214,7 +1216,8 @@ const createAliasWorkspaceFixture = () => } yield* fs.writeFileString( parentConfigPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(updatedConfig)) + '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(updatedConfig)) + + '\n', ) const parentLockPath = EffectPath.ops.join( @@ -1283,7 +1286,7 @@ const createNestedMegarepoLockRefMatchFixture = () => yield* initGitRepo(childPath) yield* fs.writeFileString( EffectPath.ops.join(childPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { 'shared-dev': 'https://example.com/acme/shared#dev', }, @@ -1310,7 +1313,7 @@ const createNestedMegarepoLockRefMatchFixture = () => yield* initGitRepo(parentPath) yield* fs.writeFileString( EffectPath.ops.join(parentPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { 'shared-main': 'https://example.com/acme/shared#main', 'shared-dev': 'https://example.com/acme/shared#dev', @@ -1388,7 +1391,7 @@ describe('nested megarepo.lock sync scope', () => { const afterNestedLock = Option.getOrThrow(afterNestedLockOpt) expect(afterNestedLock.members['shared']?.commit).toBe(staleNestedCommit) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -1424,7 +1427,7 @@ describe('nested megarepo.lock sync scope', () => { const afterNestedLock = Option.getOrThrow(afterNestedLockOpt) expect(afterNestedLock.members['shared']?.commit).toBe(staleNestedCommit) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -1460,7 +1463,7 @@ describe('nested megarepo.lock sync scope', () => { const afterNestedLock = Option.getOrThrow(afterNestedLockOpt) expect(afterNestedLock.members['shared']?.commit).toBe(sharedCommit) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -1503,7 +1506,7 @@ describe('nested megarepo.lock sync scope', () => { expect(result.exitCode).toBe(0) expect(result.stderr).not.toContain('invalid reference') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -1539,7 +1542,7 @@ describe('nested megarepo.lock sync scope', () => { const afterNestedLock = Option.getOrThrow(afterNestedLockOpt) expect(afterNestedLock.members['shared']?.commit).toBe(staleNestedCommit) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -1575,7 +1578,7 @@ describe('nested megarepo.lock sync scope', () => { const afterNestedLock = Option.getOrThrow(afterNestedLockOpt) expect(afterNestedLock.members['shared']?.commit).toBe(staleNestedCommit) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -1611,7 +1614,7 @@ describe('nested megarepo.lock sync scope', () => { const afterNestedLock = Option.getOrThrow(afterNestedLockOpt) expect(afterNestedLock.members['shared-dev']?.commit).toBe(devCommit) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -1647,7 +1650,7 @@ describe('nested megarepo.lock sync scope', () => { const afterNestedLock = Option.getOrThrow(afterNestedLockOpt) expect(afterNestedLock.members['shared']?.commit).toBe(sharedCommit) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -1707,7 +1710,7 @@ describe('mr lock', () => { // For local paths, status is 'synced' since they create symlinks expect(['synced', 'recorded', 'already_synced']).toContain(memberResult?.status) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1761,7 +1764,7 @@ describe('mr lock', () => { // After first sync, should be already_synced or synced expect(['synced', 'already_synced']).toContain(memberResult?.status) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1791,7 +1794,7 @@ describe('mr lock', () => { }, } const configContent = yield* Schema.encode( - Schema.parseJson(MegarepoConfig, { space: 2 }), + Schema.fromJsonString(MegarepoConfig, { space: 2 }), )(config) yield* fs.writeFileString( EffectPath.ops.join( @@ -1815,7 +1818,7 @@ describe('mr lock', () => { // The command should complete (might error on clone attempt, but shouldn't hang on fetch) expect(result.exitCode).toBeDefined() }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1872,8 +1875,9 @@ describe('mr lock', () => { ) yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(initialConfig)) + - '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + initialConfig, + )) + '\n', ) yield* addCommit({ repoPath: workspacePath, message: 'Initialize megarepo' }) @@ -1907,8 +1911,9 @@ describe('mr lock', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(updatedConfig)) + - '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + updatedConfig, + )) + '\n', ) // Sync again - should update symlink @@ -1937,7 +1942,7 @@ describe('mr lock', () => { expect(featureFileInFeature).toBe(true) expect(updatedLink.replace(/\/$/, '')).toBe(featureRepoPath.replace(/\/$/, '')) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -1985,7 +1990,7 @@ describe('mr lock', () => { ) yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { 'my-lib': mainRepoPath }, })) + '\n', ) @@ -2003,7 +2008,7 @@ describe('mr lock', () => { // Update config to point to feature branch yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { 'my-lib': featureRepoPath }, })) + '\n', ) @@ -2028,7 +2033,7 @@ describe('mr lock', () => { const currentLink = yield* fs.readLink(symlinkPath) expect(currentLink.replace(/\/$/, '')).toBe(mainRepoPath.replace(/\/$/, '')) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -2114,7 +2119,7 @@ describe('mr lock', () => { // The uncommitted work is still protected — apply reports, it does not clobber. expect(yield* fs.readLink(symlinkPath)).toBe(pinnedLink) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -2169,7 +2174,7 @@ describe('mr lock', () => { expect(json.results[0]?.status).not.toBe('error') expect(result.exitCode).toBe(0) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -2215,7 +2220,7 @@ describe('mr lock', () => { ) yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { 'my-lib': mainRepoPath }, })) + '\n', ) @@ -2233,7 +2238,7 @@ describe('mr lock', () => { // Update config yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))({ + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ members: { 'my-lib': featureRepoPath }, })) + '\n', ) @@ -2257,7 +2262,7 @@ describe('mr lock', () => { const currentLink = yield* fs.readLink(symlinkPath) expect(currentLink.replace(/\/$/, '')).toBe(featureRepoPath.replace(/\/$/, '')) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -2323,7 +2328,7 @@ describe('mr fetch', () => { // Should complete without error expect(result.exitCode).toBeDefined() }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -2385,7 +2390,7 @@ describe('mr fetch', () => { expect(json.results).toHaveLength(1) // Note: Local path sources behave differently, but this documents the behavior }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -2423,7 +2428,7 @@ describe('mr fetch', () => { expect(lockFile.members['test-repo']?.commit).toBe(currentCommit) expect(lockFile.members['test-repo']?.pinned).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -2450,7 +2455,7 @@ describe('mr fetch', () => { expect(memberResult.status).toBe('skipped') expect(memberResult.message).toContain('pinned') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -2479,7 +2484,7 @@ describe('mr fetch', () => { expect(memberResult.message).toContain(`'${staleCommit.slice(0, 8)}'`) expect(memberResult.message).toContain('not available locally or on the remote') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -2605,7 +2610,7 @@ describe('mr fetch', () => { const worktreeHeadAfter = yield* runGitCommand(worktreePath, 'rev-parse', 'HEAD') expect(worktreeHeadAfter).toBe(newCommit) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 30_000 }, @@ -2647,9 +2652,9 @@ describe('sync status types', () => { 'new-lib': localRepoPath, }, } - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - config, - ) + const configContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(config) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), configContent + '\n', @@ -2672,7 +2677,7 @@ describe('sync status types', () => { // For local paths, first sync creates a symlink - status is 'synced' expect(memberResult?.status).toBe('synced') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -2706,9 +2711,9 @@ describe('sync error handling', () => { 'non-existent-repo': pathToFileURL(missingRemotePath).href, }, } - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - config, - ) + const configContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(config) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), configContent + '\n', @@ -2749,7 +2754,7 @@ describe('sync error handling', () => { memberResult?.message?.toLowerCase().includes('auth'), ).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -2799,7 +2804,7 @@ describe('sync member filtering', () => { }, } const configContent = yield* Schema.encode( - Schema.parseJson(MegarepoConfig, { space: 2 }), + Schema.fromJsonString(MegarepoConfig, { space: 2 }), )(config) yield* fs.writeFileString( EffectPath.ops.join( @@ -2824,7 +2829,7 @@ describe('sync member filtering', () => { expect(json.results).toHaveLength(1) expect(json.results[0]?.name).toBe('repo1') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), 20_000, @@ -2870,7 +2875,7 @@ describe('sync member filtering', () => { }, } const configContent = yield* Schema.encode( - Schema.parseJson(MegarepoConfig, { space: 2 }), + Schema.fromJsonString(MegarepoConfig, { space: 2 }), )(config) yield* fs.writeFileString( EffectPath.ops.join( @@ -2895,7 +2900,7 @@ describe('sync member filtering', () => { expect(json.results).toHaveLength(1) expect(json.results[0]?.name).toBe('repo1') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), 20_000, @@ -2924,7 +2929,7 @@ describe('sync member filtering', () => { }, } const configContent = yield* Schema.encode( - Schema.parseJson(MegarepoConfig, { space: 2 }), + Schema.fromJsonString(MegarepoConfig, { space: 2 }), )(config) yield* fs.writeFileString( EffectPath.ops.join( @@ -2955,7 +2960,7 @@ describe('sync member filtering', () => { expect(failureMessages.toLowerCase()).toContain('mutually exclusive') } }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -3022,9 +3027,9 @@ describe('sync worktree ref mismatch detection', () => { 'test-repo': 'https://example.com/org/test-repo#main', }, } - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - config, - ) + const configContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(config) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), configContent + '\n', @@ -3087,7 +3092,7 @@ describe('sync worktree ref mismatch detection', () => { // It proceeds (possibly falling back to commit worktree) instead of returning 'skipped'. expect(memberResult?.status).not.toBe('skipped') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -3128,9 +3133,9 @@ describe('sync worktree ref mismatch detection', () => { 'test-repo': 'https://example.com/org/test-repo#main', }, } - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( - config, - ) + const configContent = yield* Schema.encode( + Schema.fromJsonString(MegarepoConfig, { space: 2 }), + )(config) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), configContent + '\n', @@ -3192,7 +3197,7 @@ describe('sync worktree ref mismatch detection', () => { // It proceeds (possibly falling back to commit worktree) instead of returning 'skipped'. expect(memberResult?.status).not.toBe('skipped') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), { timeout: 15_000 }, @@ -3245,8 +3250,9 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(initialConfig)) + - '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + initialConfig, + )) + '\n', ) yield* addCommit({ repoPath: workspacePath, @@ -3277,8 +3283,9 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(updatedConfig)) + - '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + updatedConfig, + )) + '\n', ) // Second sync - should detect and remove orphaned repo2 symlink @@ -3303,7 +3310,7 @@ describe('sync member removal detection', () => { // Verify repo2 symlink was removed expect(yield* fs.exists(repo2Symlink)).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -3353,8 +3360,9 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(initialConfig)) + - '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + initialConfig, + )) + '\n', ) yield* addCommit({ repoPath: workspacePath, @@ -3372,8 +3380,9 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(updatedConfig)) + - '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + updatedConfig, + )) + '\n', ) // Sync with --dry-run - should report removed but not actually remove @@ -3397,7 +3406,7 @@ describe('sync member removal detection', () => { ) expect(yield* fs.exists(repo2Symlink)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -3447,7 +3456,8 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(config)) + '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(config)) + + '\n', ) yield* addCommit({ repoPath: workspacePath, @@ -3475,7 +3485,7 @@ describe('sync member removal detection', () => { ) expect(yield* fs.exists(repo2Symlink)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -3517,7 +3527,8 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(config)) + '\n', + (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(config)) + + '\n', ) yield* addCommit({ repoPath: workspacePath, @@ -3552,7 +3563,7 @@ describe('sync member removal detection', () => { // The directory should still exist expect(yield* fs.exists(orphanDirPath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/lib/config.ts b/packages/@overeng/megarepo/src/lib/config.ts index 2c205f74b6..1ec0eab2b7 100644 --- a/packages/@overeng/megarepo/src/lib/config.ts +++ b/packages/@overeng/megarepo/src/lib/config.ts @@ -13,8 +13,8 @@ * - Local path: "./path", "../path", "/absolute/path" */ -import { FileSystem } from '@effect/platform' -import { Effect, JSONSchema, Option, Schema } from 'effect' +import { Effect, JsonSchema, Option, Schema } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { EffectPath, @@ -91,7 +91,7 @@ export class VscodeGeneratorConfig extends Schema.Class( * * @example { "editor.formatOnSave": true } */ - settings: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + settings: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) {} /** @@ -152,7 +152,7 @@ export class MegarepoConfig extends Schema.Class('MegarepoConfig $schema: Schema.optional(Schema.String), /** Members: repos to include in this megarepo (name -> source string) */ - members: Schema.Record({ key: Schema.String, value: Schema.String }), + members: Schema.Record(Schema.String, Schema.String), /** Generators: optional config file generation */ generators: Schema.optional(GeneratorsConfig), @@ -218,13 +218,18 @@ export const getMemberPath = ({ // ============================================================================= /** Error when no megarepo config file is found */ -export class ConfigNotFoundError extends Schema.TaggedError()( +export class ConfigNotFoundError extends Schema.TaggedErrorClass()( 'ConfigNotFoundError', { megarepoRoot: Schema.String, - message: Schema.optionalWith(Schema.String, { - default: () => 'No megarepo config found (checked megarepo.kdl and megarepo.json)', - }), + message: Schema.String.pipe( + Schema.withDecodingDefaultType( + Effect.sync(() => 'No megarepo config found (checked megarepo.kdl and megarepo.json)'), + ), + Schema.withConstructorDefault( + Effect.sync(() => 'No megarepo config found (checked megarepo.kdl and megarepo.json)'), + ), + ), }, ) {} @@ -257,7 +262,7 @@ export const readMegarepoConfig = (megarepoRoot: AbsoluteDirPath) => const format: ConfigFormat = fileName.endsWith('.kdl') === true ? 'kdl' : 'json' const config = yield* Schema.decodeUnknown( - format === 'kdl' ? MegarepoConfigFromKdl : Schema.parseJson(MegarepoConfig), + format === 'kdl' ? MegarepoConfigFromKdl : Schema.fromJsonString(MegarepoConfig), )(content) return { config, format, path: configPath } as const @@ -284,7 +289,7 @@ export const writeMegarepoConfig = ({ const content = format === 'kdl' ? yield* Schema.encode(MegarepoConfigFromKdl)(config) - : (yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))(config)) + '\n' + : (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(config)) + '\n' yield* fs.writeFileString(configPath, content) }) @@ -294,7 +299,7 @@ export const writeMegarepoConfig = ({ // ============================================================================= /** Generate JSON Schema from Effect Schema */ -export const generateJsonSchema = () => JSONSchema.make(MegarepoConfig) +export const generateJsonSchema = () => JsonSchema.make(MegarepoConfig) // ============================================================================= // Member Name Validation diff --git a/packages/@overeng/megarepo/src/lib/generators/schema.ts b/packages/@overeng/megarepo/src/lib/generators/schema.ts index 6281a21d04..8ef8689e68 100644 --- a/packages/@overeng/megarepo/src/lib/generators/schema.ts +++ b/packages/@overeng/megarepo/src/lib/generators/schema.ts @@ -5,7 +5,7 @@ * Output: schema/megarepo.schema.json in the specified location. */ -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { diff --git a/packages/@overeng/megarepo/src/lib/generators/vscode.ts b/packages/@overeng/megarepo/src/lib/generators/vscode.ts index 43f7897e35..6e3d278284 100644 --- a/packages/@overeng/megarepo/src/lib/generators/vscode.ts +++ b/packages/@overeng/megarepo/src/lib/generators/vscode.ts @@ -5,7 +5,7 @@ * Output: .vscode/megarepo.code-workspace in the megarepo root. */ -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { diff --git a/packages/@overeng/megarepo/src/lib/git-memory.integration.test.ts b/packages/@overeng/megarepo/src/lib/git-memory.integration.test.ts index 64377007cc..cdcc1eba16 100644 --- a/packages/@overeng/megarepo/src/lib/git-memory.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/git-memory.integration.test.ts @@ -22,9 +22,9 @@ import { platform } from 'node:os' import { join } from 'node:path' import { fileURLToPath } from 'node:url' -import { Command } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' +import { ChildProcess as Command } from 'effect/unstable/process' import { Effect, Schema } from 'effect' import { expect } from 'vitest' @@ -40,7 +40,7 @@ const ProbeOutput = Schema.Struct({ vmHwmKb: Schema.Number, changesCount: Schema.Number, }) -const decodeProbe = Schema.decodeUnknownSync(Schema.parseJson(ProbeOutput)) +const decodeProbe = Schema.decodeUnknownSync(Schema.fromJsonString(ProbeOutput)) const probeScript = fileURLToPath(new URL('../test-utils/memory-probe.ts', import.meta.url)) @@ -95,7 +95,7 @@ describe('git memory regression', () => { const growthKb = result.vmHwmKb - result.rssStartKb expect(result.changesCount).toBe(UNTRACKED_FILE_COUNT) expect(growthKb).toBeLessThan(MAX_GROWTH_KB) - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), { timeout: 120_000 }, ) }) diff --git a/packages/@overeng/megarepo/src/lib/git-streaming-parsers.integration.test.ts b/packages/@overeng/megarepo/src/lib/git-streaming-parsers.integration.test.ts index a33a2b133a..529ef3b13c 100644 --- a/packages/@overeng/megarepo/src/lib/git-streaming-parsers.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/git-streaming-parsers.integration.test.ts @@ -11,8 +11,9 @@ * helper is proven separately by `git-memory.integration.test.ts`. */ -import { Command, FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect, Option } from 'effect' import { expect } from 'vitest' @@ -74,7 +75,7 @@ describe('streaming git parsers', () => { expect(w.path.length).toBeGreaterThan(0) expect(w.head).toMatch(/^[0-9a-f]{40}$/) } - }).pipe(Effect.provide(NodeContext.layer), Effect.scoped), + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ) it.effect('revListUnpushed returns exactly the commits absent from remotes', () => @@ -129,6 +130,6 @@ describe('streaming git parsers', () => { expect([...unpushed].sort()).toEqual([...expected].sort()) // No empty entries leaked through the line fold. for (const sha of unpushed) expect(sha).toMatch(/^[0-9a-f]{40}$/) - }).pipe(Effect.provide(NodeContext.layer), Effect.scoped), + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ) }) diff --git a/packages/@overeng/megarepo/src/lib/git-timeout.integration.test.ts b/packages/@overeng/megarepo/src/lib/git-timeout.integration.test.ts index fdab170c04..34200b3040 100644 --- a/packages/@overeng/megarepo/src/lib/git-timeout.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/git-timeout.integration.test.ts @@ -14,8 +14,9 @@ * used to break for large members. */ -import { Command, FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect } from 'effect' import { expect } from 'vitest' @@ -92,7 +93,7 @@ describe('git operation-aware timeout', () => { // Sanity: the clone actually populated a usable bare repo. const commit = yield* Git.getCurrentCommit(target) expect(commit).toMatch(/^[0-9a-f]{40}$/) - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) it.scopedLive('a network op is bounded by the network budget (1ms → times out)', () => @@ -106,7 +107,7 @@ describe('git operation-aware timeout', () => { ).pipe(Effect.flip) expect(error).toBeInstanceOf(GitCommandTimeoutError) expect((error as GitCommandTimeoutError).timeoutMillis).toBe(1) - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) it.scopedLive('the network budget does NOT bound a local op (rev-parse still succeeds)', () => @@ -121,6 +122,6 @@ describe('git operation-aware timeout', () => { Git.getCurrentCommit(target), ) expect(commit).toMatch(/^[0-9a-f]{40}$/) - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) }) diff --git a/packages/@overeng/megarepo/src/lib/git.ts b/packages/@overeng/megarepo/src/lib/git.ts index ec378e7f6a..6db5f16ffd 100644 --- a/packages/@overeng/megarepo/src/lib/git.ts +++ b/packages/@overeng/megarepo/src/lib/git.ts @@ -4,7 +4,7 @@ * Provides Effect-wrapped git operations for cloning, fetching, and managing worktrees. */ -import { Command } from '@effect/platform' +import { ChildProcess as Command } from 'effect/unstable/process' import { Cause, Chunk, Duration, Effect, Option, Schedule, Sink, Stream } from 'effect' import * as Observability from './observability.ts' diff --git a/packages/@overeng/megarepo/src/lib/json-wire-baseline.test.ts b/packages/@overeng/megarepo/src/lib/json-wire-baseline.test.ts index 8f63686f5a..5345d5a303 100644 --- a/packages/@overeng/megarepo/src/lib/json-wire-baseline.test.ts +++ b/packages/@overeng/megarepo/src/lib/json-wire-baseline.test.ts @@ -6,10 +6,10 @@ import { MegarepoConfig } from './config.ts' import { LockFile } from './lock.ts' const encodeJson = (schema: Schema.Schema, value: A): string => - Schema.encodeSync(Schema.parseJson(schema, { space: 2 }))(value) + Schema.encodeSync(Schema.fromJsonString(schema, { space: 2 }))(value) const decodeJson = (schema: Schema.Schema, encoded: string): A => - Schema.decodeUnknownSync(Schema.parseJson(schema))(encoded) + Schema.decodeUnknownSync(Schema.fromJsonString(schema))(encoded) const roundTrip = (schema: Schema.Schema, value: A) => { const encoded = encodeJson(schema, value) diff --git a/packages/@overeng/megarepo/src/lib/lock.ts b/packages/@overeng/megarepo/src/lib/lock.ts index 40fb655348..7ed3044b0e 100644 --- a/packages/@overeng/megarepo/src/lib/lock.ts +++ b/packages/@overeng/megarepo/src/lib/lock.ts @@ -9,9 +9,10 @@ * Note: Local path sources are NOT in the lock file - they're already local. */ -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' import type { ParseResult } from 'effect' import { Effect, Option, Schema } from 'effect' +import { FileSystem } from 'effect/FileSystem' import type { AbsoluteFilePath } from '@overeng/effect-path' @@ -53,7 +54,7 @@ export class LockFile extends Schema.Class('LockFile')({ version: Schema.Number, /** Locked members (name -> entry) */ - members: Schema.Record({ key: Schema.String, value: LockedMember }), + members: Schema.Record(Schema.String, LockedMember), }) {} // ============================================================================= @@ -79,7 +80,7 @@ export const readLockFile = ( } const content = yield* fs.readFileString(lockPath) - const parsed = yield* Schema.decodeUnknown(Schema.parseJson(LockFile))(content) + const parsed = yield* Schema.decodeUnknown(Schema.fromJsonString(LockFile))(content) return Option.some(parsed) }) @@ -99,7 +100,7 @@ export const writeLockFile = ({ > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem - const content = yield* Schema.encode(Schema.parseJson(LockFile, { space: 2 }))(lockFile) + const content = yield* Schema.encode(Schema.fromJsonString(LockFile, { space: 2 }))(lockFile) yield* fs.writeFileString(lockPath, content + '\n') }) diff --git a/packages/@overeng/megarepo/src/lib/megarepo-traversal.ts b/packages/@overeng/megarepo/src/lib/megarepo-traversal.ts index d202b3c5f4..e4873b3fe4 100644 --- a/packages/@overeng/megarepo/src/lib/megarepo-traversal.ts +++ b/packages/@overeng/megarepo/src/lib/megarepo-traversal.ts @@ -6,15 +6,16 @@ * Raw traversal paths can grow forever in symlink cycles. */ -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' import { Effect, Ref, Schema, type ParseResult } from 'effect' +import { FileSystem } from 'effect/FileSystem' import type { AbsoluteDirPath } from '@overeng/effect-path' import * as Observability from './observability.ts' /** Known command/operation families that perform nested megarepo traversal. */ -export const MegarepoTraversalPurpose = Schema.Literal('status', 'ls', 'sync').annotations({ +export const MegarepoTraversalPurpose = Schema.Literals(['status', 'ls', 'sync']).annotate({ identifier: 'Megarepo.Traversal.Purpose', }) export type MegarepoTraversalPurpose = typeof MegarepoTraversalPurpose.Type @@ -22,7 +23,7 @@ export type MegarepoTraversalPurpose = typeof MegarepoTraversalPurpose.Type /** Branded canonical identity for a traversed megarepo root. */ export const MegarepoTraversalNodeKey = Schema.NonEmptyString.pipe( Schema.brand('Megarepo.Traversal.NodeKey'), - Schema.annotations({ identifier: 'Megarepo.Traversal.NodeKey' }), + Schema.annotate({ identifier: 'Megarepo.Traversal.NodeKey' }), ) export type MegarepoTraversalNodeKey = typeof MegarepoTraversalNodeKey.Type diff --git a/packages/@overeng/megarepo/src/lib/nix-lock/input-discovery.ts b/packages/@overeng/megarepo/src/lib/nix-lock/input-discovery.ts index dbb54909d2..d5e8a5856c 100644 --- a/packages/@overeng/megarepo/src/lib/nix-lock/input-discovery.ts +++ b/packages/@overeng/megarepo/src/lib/nix-lock/input-discovery.ts @@ -5,7 +5,8 @@ * which megarepo members are referenced as inputs by other members. */ -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' diff --git a/packages/@overeng/megarepo/src/lib/nix-lock/mod.ts b/packages/@overeng/megarepo/src/lib/nix-lock/mod.ts index 6715008e28..b647fac0e1 100644 --- a/packages/@overeng/megarepo/src/lib/nix-lock/mod.ts +++ b/packages/@overeng/megarepo/src/lib/nix-lock/mod.ts @@ -8,13 +8,13 @@ * source of truth for dependency versions. */ -import { - Command, - type CommandExecutor, - FileSystem, - type Error as PlatformError, -} from '@effect/platform' +import type { Error as PlatformError } from 'effect' import { Effect, Option, Schema, type ParseResult } from 'effect' +import { FileSystem } from 'effect/FileSystem' +import { + ChildProcess as Command, + type ChildProcessSpawner as CommandExecutor, +} from 'effect/unstable/process' import { EffectPath, type AbsoluteDirPath, type AbsoluteFilePath } from '@overeng/effect-path' @@ -127,14 +127,11 @@ const DEVENV_LOCK = 'devenv.lock' const MEGAREPO_LOCK = LOCK_FILE_NAME /** Schema for raw flake lock JSON (used to preserve key order during manipulation) */ -const RawFlakeLockJson = Schema.parseJson( +const RawFlakeLockJson = Schema.fromJsonString( Schema.mutable( Schema.Struct({ nodes: Schema.mutable( - Schema.Record({ - key: Schema.String, - value: Schema.mutable(Schema.Record({ key: Schema.String, value: Schema.Unknown })), - }), + Schema.Record(Schema.String, Schema.mutable(Schema.Record(Schema.String, Schema.Unknown))), ), root: Schema.String, version: Schema.Number, @@ -149,8 +146,8 @@ const RawFlakeLockJson = Schema.parseJson( * with 2-space indentation, matching the prior `JSON.stringify(value, null, 2)` * output byte-for-byte. */ -const RawLockJson = Schema.parseJson( - Schema.mutable(Schema.Record({ key: Schema.String, value: Schema.Unknown })), +const RawLockJson = Schema.fromJsonString( + Schema.mutable(Schema.Record(Schema.String, Schema.Unknown)), { space: 2 }, ) @@ -163,7 +160,7 @@ const encodeRawLockJson = (value: Record): string => // ============================================================================= /** Schema for nix flake prefetch JSON output (parses JSON string directly) */ -const NixFlakePrefetchOutput = Schema.parseJson( +const NixFlakePrefetchOutput = Schema.fromJsonString( Schema.Struct({ hash: Schema.String, locked: Schema.Struct({ @@ -177,7 +174,7 @@ const NixFlakePrefetchOutput = Schema.parseJson( ) /** Error for Nix flake metadata fetch failures */ -export class NixFlakeMetadataError extends Schema.TaggedError()( +export class NixFlakeMetadataError extends Schema.TaggedErrorClass()( 'NixFlakeMetadataError', { message: Schema.String, @@ -726,7 +723,7 @@ const deepEqual = ({ a, b }: { a: unknown; b: unknown }): boolean => { } /** Error for shared input source configuration issues */ -export class SharedInputSourceError extends Schema.TaggedError()( +export class SharedInputSourceError extends Schema.TaggedErrorClass()( 'SharedInputSourceError', { message: Schema.String, diff --git a/packages/@overeng/megarepo/src/lib/nix-lock/schema.ts b/packages/@overeng/megarepo/src/lib/nix-lock/schema.ts index 324af771d1..1df4cd8030 100644 --- a/packages/@overeng/megarepo/src/lib/nix-lock/schema.ts +++ b/packages/@overeng/megarepo/src/lib/nix-lock/schema.ts @@ -76,12 +76,12 @@ export class IndirectLockedInput extends Schema.Class('Indi * Union of all locked input types we support * Note: Nix supports more types (tarball, file, etc.) but these are the common ones */ -export const LockedInput = Schema.Union( +export const LockedInput = Schema.Union([ GitHubLockedInput, GitLockedInput, PathLockedInput, IndirectLockedInput, -) +]) export type LockedInput = typeof LockedInput.Type /** @@ -93,7 +93,7 @@ export const GenericLockedInput = Schema.Struct({ rev: Schema.optional(Schema.String), narHash: Schema.optional(Schema.String), lastModified: Schema.optional(Schema.Number), -}).pipe(Schema.annotations({ identifier: 'GenericLockedInput' })) +}).pipe(Schema.annotate({ identifier: 'GenericLockedInput' })) export type GenericLockedInput = typeof GenericLockedInput.Type // ============================================================================= @@ -112,19 +112,19 @@ export class FlakeLockNode extends Schema.Class('FlakeLockNode')( * Resolved/locked input data * Contains the exact revision and optional integrity info (narHash, lastModified) */ - locked: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + locked: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), /** * Original input specification (as written in flake.nix) * Preserved for reference but not used during evaluation */ - original: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + original: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), /** * Map of input names to node names in this lock file * Used to resolve transitive dependencies */ - inputs: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), + inputs: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), /** * Whether this input is a flake (default: true) @@ -145,7 +145,7 @@ export class FlakeLock extends Schema.Class('FlakeLock')({ * Map of node names to node data * The "root" node is special and represents the flake itself */ - nodes: Schema.Record({ key: Schema.String, value: FlakeLockNode }), + nodes: Schema.Record(Schema.String, FlakeLockNode), /** * Name of the root node (always "root" in practice) diff --git a/packages/@overeng/megarepo/src/lib/source-policy.ts b/packages/@overeng/megarepo/src/lib/source-policy.ts index 7ee76469b2..4407e52082 100644 --- a/packages/@overeng/megarepo/src/lib/source-policy.ts +++ b/packages/@overeng/megarepo/src/lib/source-policy.ts @@ -1,4 +1,5 @@ -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' diff --git a/packages/@overeng/megarepo/src/lib/source-policy.unit.test.ts b/packages/@overeng/megarepo/src/lib/source-policy.unit.test.ts index d5690e2865..21db9af33a 100644 --- a/packages/@overeng/megarepo/src/lib/source-policy.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/source-policy.unit.test.ts @@ -1,5 +1,5 @@ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { Effect, type Scope } from 'effect' import { describe, expect, it } from 'vitest' @@ -30,7 +30,7 @@ const makeLockFile = (): LockFile => }) as LockFile const runWithContext = (effect: Effect.Effect) => - Effect.runPromise(effect.pipe(Effect.scoped, Effect.provide(NodeContext.layer))) + Effect.runPromise(effect.pipe(Effect.scoped, Effect.provide(NodeServices.layer))) const writeFile = ({ root, diff --git a/packages/@overeng/megarepo/src/lib/store-archive.integration.test.ts b/packages/@overeng/megarepo/src/lib/store-archive.integration.test.ts index 4989c5f06f..02413cc2ef 100644 --- a/packages/@overeng/megarepo/src/lib/store-archive.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-archive.integration.test.ts @@ -14,8 +14,9 @@ * contain `-`/`--`/`/`; only a trailing valid ISO8601 instant is a timestamp). */ -import { Command, FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect, Option } from 'effect' import { expect } from 'vitest' @@ -121,7 +122,7 @@ describe('store-archive: archiveWorktree', () => { yield* git(bareRepoPath, 'worktree', 'add', reAddPath, 'feature/x') expect(yield* fs.exists(reAddPath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -180,7 +181,7 @@ describe('store-archive: archiveWorktree', () => { yield* git(bareRepoPath, 'worktree', 'add', reAddPath, 'feature/prod') expect(yield* fs.exists(reAddPath)).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -230,7 +231,7 @@ describe('store-archive: archiveWorktree', () => { const status = yield* Git.getWorktreeStatus(dest) expect(status.isDirty).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -278,7 +279,7 @@ describe('store-archive: scanArchives + reapArchive', () => { expect(entry.path.includes('/.archive/')).toBe(true) } }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -311,7 +312,7 @@ describe('store-archive: scanArchives + reapArchive', () => { expect(yield* fs.exists(archivePath)).toBe(false) expect((yield* scanArchives({ repoRoot, bareRepoPath })).length).toBe(0) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -350,7 +351,7 @@ describe('store-archive: scanArchives + reapArchive', () => { .map((entry) => entry.branch) expect(eligible).toEqual(['feature/stale']) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/lib/store-archive.ts b/packages/@overeng/megarepo/src/lib/store-archive.ts index 4e31ce7680..7aa291180c 100644 --- a/packages/@overeng/megarepo/src/lib/store-archive.ts +++ b/packages/@overeng/megarepo/src/lib/store-archive.ts @@ -31,8 +31,9 @@ * on this persistence path. */ -import type { CommandExecutor } from '@effect/platform' -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' diff --git a/packages/@overeng/megarepo/src/lib/store-fs-atomic.ts b/packages/@overeng/megarepo/src/lib/store-fs-atomic.ts index 28caf8ac3f..61baea22bc 100644 --- a/packages/@overeng/megarepo/src/lib/store-fs-atomic.ts +++ b/packages/@overeng/megarepo/src/lib/store-fs-atomic.ts @@ -10,7 +10,8 @@ import { randomBytes } from 'node:crypto' -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { EffectPath, type AbsoluteFilePath } from '@overeng/effect-path' diff --git a/packages/@overeng/megarepo/src/lib/store-fs-atomic.unit.test.ts b/packages/@overeng/megarepo/src/lib/store-fs-atomic.unit.test.ts index b8c95f48f3..5a26603123 100644 --- a/packages/@overeng/megarepo/src/lib/store-fs-atomic.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-fs-atomic.unit.test.ts @@ -7,8 +7,8 @@ * no `.tmp-*` sibling lingering as garbage (the `tapError` cleanup branch). */ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect } from 'effect' import { expect } from 'vitest' @@ -33,7 +33,7 @@ describe('store-fs-atomic: writeFileAtomic', () => { const remaining = yield* fs.readDirectory(dir) expect(remaining.filter((name) => name.includes('.tmp-'))).toEqual([]) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -65,7 +65,7 @@ describe('store-fs-atomic: writeFileAtomic', () => { const remaining = yield* fs.readDirectory(dir) expect(remaining.filter((name) => name.includes('.tmp-'))).toEqual([]) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -95,7 +95,7 @@ describe('store-fs-atomic: writeFileAtomic', () => { const remaining = yield* fs.readDirectory(dir) expect(remaining.filter((name) => name.includes('.tmp-'))).toEqual([]) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/lib/store-gc-config.ts b/packages/@overeng/megarepo/src/lib/store-gc-config.ts index 478d8bfa51..8c1a008986 100644 --- a/packages/@overeng/megarepo/src/lib/store-gc-config.ts +++ b/packages/@overeng/megarepo/src/lib/store-gc-config.ts @@ -7,7 +7,8 @@ * unknown/invalid files fall back to the defaults (never fail the gc path). */ -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Effect, Schema } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -83,7 +84,7 @@ export const loadStoreGcConfig = ({ const path = gcConfigPath(storeBasePath) const override = yield* fs.readFileString(path).pipe( Effect.flatMap((content) => - Schema.decodeUnknown(Schema.parseJson(StoreGcConfigOverride))(content), + Schema.decodeUnknown(Schema.fromJsonString(StoreGcConfigOverride))(content), ), Effect.orElseSucceed(() => ({}) as StoreGcConfigOverride), ) diff --git a/packages/@overeng/megarepo/src/lib/store-gc-config.unit.test.ts b/packages/@overeng/megarepo/src/lib/store-gc-config.unit.test.ts index d531a35f46..2c81a589db 100644 --- a/packages/@overeng/megarepo/src/lib/store-gc-config.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-gc-config.unit.test.ts @@ -1,5 +1,5 @@ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { it as effectIt } from '@effect/vitest' import { Effect } from 'effect' import { describe, expect, it } from 'vitest' @@ -78,7 +78,7 @@ describe('store-gc-config', () => { ) expect(yield* loadStoreGcConfig({ storeBasePath })).toEqual(DEFAULT_STORE_GC_CONFIG) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -96,7 +96,7 @@ describe('store-gc-config', () => { archiveRetentionMs: 5678, }) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -109,7 +109,7 @@ describe('store-gc-config', () => { // Degrades to defaults rather than failing the gc path. expect(yield* loadStoreGcConfig({ storeBasePath })).toEqual(DEFAULT_STORE_GC_CONFIG) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/lib/store-gc-observations.ts b/packages/@overeng/megarepo/src/lib/store-gc-observations.ts index 10ad11369f..81d0a28b23 100644 --- a/packages/@overeng/megarepo/src/lib/store-gc-observations.ts +++ b/packages/@overeng/megarepo/src/lib/store-gc-observations.ts @@ -23,8 +23,9 @@ * conservatively re-arms all grace windows. */ -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' import { Effect, Schema, type ParseResult } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { EffectPath, type AbsoluteDirPath, type AbsoluteFilePath } from '@overeng/effect-path' @@ -32,7 +33,7 @@ import * as Observability from './observability.ts' import { writeFileAtomic } from './store-fs-atomic.ts' /** Ledger schema: path -> epoch-ms it was first observed continuously cold. */ -const GcObservationLedger = Schema.Record({ key: Schema.String, value: Schema.Number }) +const GcObservationLedger = Schema.Record(Schema.String, Schema.Number) /** In-memory ledger: `normalizePath(worktreePath) -> firstSeenColdAtMs`. */ export type GcObservationLedger = Schema.Schema.Type @@ -86,7 +87,7 @@ export const readObservationLedger = ({ const path = ledgerPath(storeBasePath) return yield* fs.readFileString(path).pipe( Effect.flatMap((content) => - Schema.decodeUnknown(Schema.parseJson(GcObservationLedger))(content), + Schema.decodeUnknown(Schema.fromJsonString(GcObservationLedger))(content), ), Effect.orElseSucceed(() => ({}) as GcObservationLedger), ) @@ -114,7 +115,7 @@ const writeObservationLedger = ({ const path = ledgerPath(storeBasePath) const stateDir = EffectPath.ops.join(storeBasePath, EffectPath.unsafe.relativeDir('.state/')) yield* fs.makeDirectory(stateDir, { recursive: true }) - const content = yield* Schema.encode(Schema.parseJson(GcObservationLedger, { space: 2 }))( + const content = yield* Schema.encode(Schema.fromJsonString(GcObservationLedger, { space: 2 }))( ledger, ) yield* writeFileAtomic({ path, content: content + '\n' }) diff --git a/packages/@overeng/megarepo/src/lib/store-gc-observations.unit.test.ts b/packages/@overeng/megarepo/src/lib/store-gc-observations.unit.test.ts index 88c1db9f24..b46cfd3602 100644 --- a/packages/@overeng/megarepo/src/lib/store-gc-observations.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-gc-observations.unit.test.ts @@ -1,5 +1,5 @@ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { Effect } from 'effect' import { describe, expect, it } from 'vitest' @@ -14,7 +14,7 @@ import { } from './store-gc-observations.ts' const run = (effect: Effect.Effect) => - Effect.runPromise(effect.pipe(Effect.provide(NodeContext.layer))) + Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))) const withTempStore = ( body: (storeBasePath: AbsoluteDirPath) => Effect.Effect, diff --git a/packages/@overeng/megarepo/src/lib/store-hygiene.ts b/packages/@overeng/megarepo/src/lib/store-hygiene.ts index effd2163d1..8d37b2ff53 100644 --- a/packages/@overeng/megarepo/src/lib/store-hygiene.ts +++ b/packages/@overeng/megarepo/src/lib/store-hygiene.ts @@ -5,9 +5,10 @@ * Used by pre-flight checks (sync/lock/pin) and `mr store fix`. */ -import { FileSystem, type Error as PlatformError } from '@effect/platform' -import type { CommandExecutor } from '@effect/platform' +import type { Error as PlatformError } from 'effect' import { Effect, Option, Schema } from 'effect' +import { FileSystem } from 'effect/FileSystem' +import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' import { type MegarepoConfig, @@ -68,13 +69,13 @@ export type StoreIssueMeta = // ============================================================================= /** Tagged error raised when store pre-flight checks detect blocking issues */ -export class StoreHygieneError extends Schema.TaggedError()( +export class StoreHygieneError extends Schema.TaggedErrorClass()( 'StoreHygieneError', { message: Schema.String, issues: Schema.Array( Schema.Struct({ - severity: Schema.Literal('error', 'warning', 'info'), + severity: Schema.Literals(['error', 'warning', 'info']), type: Schema.String, memberName: Schema.String, message: Schema.String, diff --git a/packages/@overeng/megarepo/src/lib/store-hygiene.unit.test.ts b/packages/@overeng/megarepo/src/lib/store-hygiene.unit.test.ts index 0440c82471..39992ab190 100644 --- a/packages/@overeng/megarepo/src/lib/store-hygiene.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-hygiene.unit.test.ts @@ -1,5 +1,5 @@ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { Effect, Option } from 'effect' import { describe, expect, it } from 'vitest' @@ -80,7 +80,7 @@ const makeTestLockFile = (members: Record(effect: Effect.Effect) => - Effect.runPromise(effect.pipe(Effect.provide(NodeContext.layer))) + Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))) // ============================================================================= // Tests diff --git a/packages/@overeng/megarepo/src/lib/store-liveness.integration.test.ts b/packages/@overeng/megarepo/src/lib/store-liveness.integration.test.ts index c5fa060106..34d744de58 100644 --- a/packages/@overeng/megarepo/src/lib/store-liveness.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-liveness.integration.test.ts @@ -1,5 +1,6 @@ -import { Command, FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect, Option } from 'effect' import { expect } from 'vitest' @@ -70,7 +71,7 @@ describe('store-liveness', () => { expect(livePaths).toEqual(new Set([normalizePath(mainWorktreePath)])) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -140,7 +141,7 @@ describe('store-liveness', () => { livePaths: [normalizePath(commitWorktreePath), normalizePath(mainWorktreePath)].sort(), }) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -206,7 +207,7 @@ describe('store-liveness', () => { expect(liveSet.paths).toContain(normalizePath(commitWorktreePath)) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -285,7 +286,7 @@ describe('store-liveness', () => { expect(record.updatedAt).toBe(new Date(1_700_000_001_000).toISOString()) expect(record.livePaths).toEqual([normalizePath(featureWorktreePath)]) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -361,7 +362,7 @@ describe('store-liveness', () => { const record = decodeJson(content) as { updatedAt: string } expect(record.updatedAt).toBe(new Date(1_700_000_000_000).toISOString()) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -415,7 +416,7 @@ describe('store-liveness', () => { const entries = yield* fs.readDirectory(registryDir) expect(entries.filter((e) => e.endsWith('.json'))).toHaveLength(0) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/lib/store-liveness.ts b/packages/@overeng/megarepo/src/lib/store-liveness.ts index f11bca7d64..f95bae1231 100644 --- a/packages/@overeng/megarepo/src/lib/store-liveness.ts +++ b/packages/@overeng/megarepo/src/lib/store-liveness.ts @@ -9,7 +9,8 @@ import { createHash } from 'node:crypto' -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option, Schema, type ParseResult } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -239,7 +240,7 @@ export const refreshWorkspaceRegistry = ({ const registryDir = workspaceRegistryDir(store) yield* fs.makeDirectory(registryDir, { recursive: true }) - const content = yield* Schema.encode(Schema.parseJson(StoreWorkspaceRecord, { space: 2 }))( + const content = yield* Schema.encode(Schema.fromJsonString(StoreWorkspaceRecord, { space: 2 }))( record, ) // Atomic (write-temp-then-rename): a concurrent reader (e.g. an under-lock @@ -305,7 +306,7 @@ const readRegistryRecords = ({ const recordPath = EffectPath.ops.join(registryDir, EffectPath.unsafe.relativeFile(entry)) const parsed = yield* fs.readFileString(recordPath).pipe( Effect.flatMap((content) => - Schema.decodeUnknown(Schema.parseJson(StoreWorkspaceRecord))(content), + Schema.decodeUnknown(Schema.fromJsonString(StoreWorkspaceRecord))(content), ), Effect.orElseSucceed(() => null), ) @@ -344,9 +345,9 @@ const readRegistryRecords = ({ updatedAt: new Date(reconcile.now).toISOString(), livePaths: [...reconciled.paths].toSorted(), } - const content = yield* Schema.encode(Schema.parseJson(StoreWorkspaceRecord, { space: 2 }))( - record, - ) + const content = yield* Schema.encode( + Schema.fromJsonString(StoreWorkspaceRecord, { space: 2 }), + )(record) // Atomic rewrite so a concurrent reader never sees a torn record and // drops a live workspace's veto right before deletion (decision 0010). yield* writeFileAtomic({ path: recordPath, content: content + '\n' }) diff --git a/packages/@overeng/megarepo/src/lib/store-lock.ts b/packages/@overeng/megarepo/src/lib/store-lock.ts index 30f1545988..49f558eac4 100644 --- a/packages/@overeng/megarepo/src/lib/store-lock.ts +++ b/packages/@overeng/megarepo/src/lib/store-lock.ts @@ -17,7 +17,7 @@ import { createHash } from 'node:crypto' -import { Context, Duration, Effect, Layer, SynchronizedRef } from 'effect' +import { Context, Duration, Effect, Layer, Semaphore, SynchronizedRef } from 'effect' import type { AbsoluteDirPath } from '@overeng/effect-path' import { DistributedSemaphore, DistributedSemaphoreBacking } from '@overeng/utils/lock' @@ -42,7 +42,9 @@ export interface StoreLockService { } /** Distributed semaphore service for serializing concurrent access to shared store resources */ -export class StoreLock extends Context.Tag('megarepo/StoreLock')() {} +export class StoreLock extends Context.Service()( + 'megarepo/StoreLock', +) {} type DistributedSem = Effect.Effect.Success> @@ -78,7 +80,7 @@ const makeKeyedLock = ({ const existing = map.get(hashedKey) if (existing !== undefined) return Effect.succeed([existing, map] as const) return Effect.gen(function* () { - const gate = yield* Effect.makeSemaphore(1) + const gate = yield* Semaphore.make(1) const distributed = yield* DistributedSemaphore.make(hashedKey, { limit: 1, ttl: LOCK_TTL, diff --git a/packages/@overeng/megarepo/src/lib/store-lossless.integration.test.ts b/packages/@overeng/megarepo/src/lib/store-lossless.integration.test.ts index 95f1a568d8..4e2b26a581 100644 --- a/packages/@overeng/megarepo/src/lib/store-lossless.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-lossless.integration.test.ts @@ -9,8 +9,9 @@ * not. */ -import { Command, FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect } from 'effect' import { expect } from 'vitest' @@ -109,7 +110,7 @@ describe('store-lossless', () => { }) expect(count).toBe(0) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -151,7 +152,7 @@ describe('store-lossless', () => { }) expect(assessment.unpushed).toBe(1) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -179,7 +180,7 @@ describe('store-lossless', () => { // Still reachable via refs/remotes/origin/main ⇒ recoverable ⇒ 0. expect(count).toBe(0) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -212,7 +213,7 @@ describe('store-lossless', () => { // No remote-tracking refs ⇒ everything reads as unpushed ⇒ keep. expect(count).toBeGreaterThan(0) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -236,7 +237,7 @@ describe('store-lossless', () => { expect(yield* hasStash({ bareRepoPath: bare })).toBe(true) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -270,7 +271,7 @@ describe('store-lossless', () => { expect(assessment).toEqual({ unpushed: 0, dirty: true, hasStash: true }) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/lib/store-pr-state.ts b/packages/@overeng/megarepo/src/lib/store-pr-state.ts index 3c0a963233..9dd8ec0349 100644 --- a/packages/@overeng/megarepo/src/lib/store-pr-state.ts +++ b/packages/@overeng/megarepo/src/lib/store-pr-state.ts @@ -20,8 +20,11 @@ * directly with fake gh output so no real `gh`/network is needed. */ -import { Command, CommandExecutor } from '@effect/platform' import { Context, Duration, Effect, Layer, Option, Schema } from 'effect' +import { + ChildProcess as Command, + ChildProcessSpawner as CommandExecutor, +} from 'effect/unstable/process' import type { RelativeDirPath } from '@overeng/effect-path' @@ -67,10 +70,9 @@ export interface PrStateResolverService { } /** PR-state resolver service tag. */ -export class PrStateResolver extends Context.Tag('megarepo/PrStateResolver')< - PrStateResolver, - PrStateResolverService ->() {} +export class PrStateResolver extends Context.Service()( + 'megarepo/PrStateResolver', +) {} // ============================================================================= // Pure seams (unit-tested directly with fake gh output) @@ -98,7 +100,7 @@ export const parseRepoCoordinates = ( const GhPr = Schema.Struct({ number: Schema.Number, /** gh emits uppercase `MERGED`/`CLOSED`/`OPEN`. */ - state: Schema.Literal('MERGED', 'CLOSED', 'OPEN'), + state: Schema.Literals(['MERGED', 'CLOSED', 'OPEN']), headRefName: Schema.String, /** ISO 8601, or `null` when not merged. */ mergedAt: Schema.NullOr(Schema.String), @@ -118,7 +120,7 @@ const GhPrList = Schema.Array(GhPr) * non-zero leaving empty stdout) ⇒ `none`, which the caller maps to keep. */ export const decodePrListJson = (raw: string): Option.Option> => - Schema.decodeUnknownOption(Schema.parseJson(GhPrList))(raw) + Schema.decodeUnknownOption(Schema.fromJsonString(GhPrList))(raw) /** ISO 8601 ⇒ epoch ms; `null`/unparseable ⇒ `undefined`. */ const isoToMs = (iso: string | null): number | undefined => { diff --git a/packages/@overeng/megarepo/src/lib/store-worktree-policy.unit.test.ts b/packages/@overeng/megarepo/src/lib/store-worktree-policy.unit.test.ts index 3f3c403e5e..a61c8d801a 100644 --- a/packages/@overeng/megarepo/src/lib/store-worktree-policy.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-worktree-policy.unit.test.ts @@ -1,5 +1,5 @@ import { describe, it } from '@effect/vitest' -import * as fc from 'effect/FastCheck' +import * as fc from 'effect/testing/FastCheck' import { expect } from 'vitest' import type { StoreGcConfig } from './store-gc-config.ts' diff --git a/packages/@overeng/megarepo/src/lib/store.ts b/packages/@overeng/megarepo/src/lib/store.ts index 94c37af7d3..d501e45e82 100644 --- a/packages/@overeng/megarepo/src/lib/store.ts +++ b/packages/@overeng/megarepo/src/lib/store.ts @@ -20,7 +20,8 @@ * ``` */ -import { FileSystem, type Error as PlatformError } from '@effect/platform' +import type { Error as PlatformError } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { Context, Effect, Layer, Option } from 'effect' import { EffectPath, type AbsoluteDirPath, type RelativeDirPath } from '@overeng/effect-path' @@ -104,7 +105,7 @@ export interface MegarepoStore { } /** Store service tag */ -export class Store extends Context.Tag('megarepo/Store')() {} +export class Store extends Context.Service()('megarepo/Store') {} // ============================================================================= // Store Implementation diff --git a/packages/@overeng/megarepo/src/lib/sync/member.ts b/packages/@overeng/megarepo/src/lib/sync/member.ts index b9adf52268..5c9acdd75e 100644 --- a/packages/@overeng/megarepo/src/lib/sync/member.ts +++ b/packages/@overeng/megarepo/src/lib/sync/member.ts @@ -6,7 +6,7 @@ import path from 'node:path' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' diff --git a/packages/@overeng/megarepo/src/lib/sync/schema.ts b/packages/@overeng/megarepo/src/lib/sync/schema.ts index 603dd0f7e1..d5d89dfecd 100644 --- a/packages/@overeng/megarepo/src/lib/sync/schema.ts +++ b/packages/@overeng/megarepo/src/lib/sync/schema.ts @@ -14,7 +14,7 @@ import { RefMismatch } from '../issues.ts' // ============================================================================= /** Member sync status */ -export const MemberSyncStatus = Schema.Literal( +export const MemberSyncStatus = Schema.Literals([ 'cloned', 'synced', 'already_synced', @@ -24,12 +24,12 @@ export const MemberSyncStatus = Schema.Literal( 'recorded', 'applied', 'removed', -) +]) /** Inferred type for the possible outcomes of syncing a single member. */ export type MemberSyncStatus = Schema.Schema.Type /** Sync mode. */ -export const SyncMode = Schema.Literal('fetch', 'apply', 'lock') +export const SyncMode = Schema.Literals(['fetch', 'apply', 'lock']) /** Inferred type for sync mode. */ export type SyncMode = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/test-utils/json.ts b/packages/@overeng/megarepo/src/test-utils/json.ts index 58ce9aa8c5..e823c9d0f1 100644 --- a/packages/@overeng/megarepo/src/test-utils/json.ts +++ b/packages/@overeng/megarepo/src/test-utils/json.ts @@ -1,7 +1,7 @@ /** * JSON helpers for tests. * - * Thin wrappers over `Schema.parseJson(Schema.Unknown)` so test fixtures encode + * Thin wrappers over `Schema.fromJsonString(Schema.Unknown)` so test fixtures encode * and assertions decode JSON through the schema path (no raw `JSON.parse` / * `JSON.stringify`). Behavior matches the plain JSON primitives for the opaque * fixture shapes exercised here. @@ -9,7 +9,7 @@ import { Schema } from 'effect' -const JsonValue = Schema.parseJson(Schema.Unknown) +const JsonValue = Schema.fromJsonString(Schema.Unknown) /** Encode an arbitrary JSON-serializable value to a JSON string (no indentation). */ export const encodeJson = (value: unknown): string => Schema.encodeSync(JsonValue)(value) diff --git a/packages/@overeng/megarepo/src/test-utils/memory-probe.ts b/packages/@overeng/megarepo/src/test-utils/memory-probe.ts index c8f3134d5e..cd8813bafe 100644 --- a/packages/@overeng/megarepo/src/test-utils/memory-probe.ts +++ b/packages/@overeng/megarepo/src/test-utils/memory-probe.ts @@ -20,7 +20,7 @@ import { readFileSync } from 'node:fs' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect } from 'effect' import * as Git from '../lib/git.ts' @@ -49,7 +49,7 @@ const program = Effect.gen(function* () { console.log(encodeJson({ rssStartKb, vmHwmKb, changesCount: status.changesCount })) }) -Effect.runPromise(program.pipe(Effect.provide(NodeContext.layer))).catch((error) => { +Effect.runPromise(program.pipe(Effect.provide(NodeServices.layer))).catch((error) => { console.error(error) process.exit(1) }) diff --git a/packages/@overeng/megarepo/src/test-utils/setup.ts b/packages/@overeng/megarepo/src/test-utils/setup.ts index 61dd281909..8e9abafab7 100644 --- a/packages/@overeng/megarepo/src/test-utils/setup.ts +++ b/packages/@overeng/megarepo/src/test-utils/setup.ts @@ -6,7 +6,8 @@ import os from 'node:os' -import { Command, FileSystem } from '@effect/platform' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' import { Effect, Schema } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -194,7 +195,7 @@ export const createWorkspace = (fixture?: WorkspaceFixture) => const config: MegarepoConfig = { members: fixture?.members ?? {}, } - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( + const configContent = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( config, ) yield* fs.writeFileString( @@ -331,7 +332,7 @@ export const readConfig = (workspacePath: AbsoluteDirPath) => EffectPath.unsafe.relativeFile('megarepo.json'), ) const content = yield* fs.readFileString(configPath) - return yield* Schema.decodeUnknown(Schema.parseJson(MegarepoConfig))(content) + return yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))(content) }) /** Generate a megarepo.json config object */ diff --git a/packages/@overeng/megarepo/src/test-utils/store-setup.integration.test.ts b/packages/@overeng/megarepo/src/test-utils/store-setup.integration.test.ts index 85f3675d07..fc6ea376b3 100644 --- a/packages/@overeng/megarepo/src/test-utils/store-setup.integration.test.ts +++ b/packages/@overeng/megarepo/src/test-utils/store-setup.integration.test.ts @@ -1,5 +1,6 @@ -import { Command, FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { ChildProcess as Command } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect } from 'effect' import { expect } from 'vitest' @@ -46,7 +47,7 @@ describe('store-setup fixtures', () => { const unpushed = yield* git(bare, 'rev-list', head, '--not', '--remotes') expect(unpushed).toBe('') }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -78,7 +79,7 @@ describe('store-setup fixtures', () => { const list = yield* git(bare, 'worktree', 'list', '--porcelain') expect(list).toContain(archivePath.replace(/\/+$/, '')) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) @@ -112,7 +113,7 @@ describe('store-setup fixtures', () => { ) expect(yield* fs.exists(registryDir)).toBe(false) }, - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.scoped, ), ) diff --git a/packages/@overeng/megarepo/src/test-utils/store-setup.ts b/packages/@overeng/megarepo/src/test-utils/store-setup.ts index fbcf210b92..4e3e9db8a8 100644 --- a/packages/@overeng/megarepo/src/test-utils/store-setup.ts +++ b/packages/@overeng/megarepo/src/test-utils/store-setup.ts @@ -4,7 +4,7 @@ * Provides helpers for creating test stores with bare repos and worktrees. */ -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect, Option, Schema } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -389,7 +389,7 @@ export const createWorkspaceWithLock = (args: { const config: MegarepoConfig = { members: args.members, } - const configContent = yield* Schema.encode(Schema.parseJson(MegarepoConfig, { space: 2 }))( + const configContent = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( config, ) yield* fs.writeFileString( diff --git a/packages/@overeng/notion-cli/nix/build.nix b/packages/@overeng/notion-cli/nix/build.nix index 84c19a683a..da80c42a68 100644 --- a/packages/@overeng/notion-cli/nix/build.nix +++ b/packages/@overeng/notion-cli/nix/build.nix @@ -33,7 +33,7 @@ let installRuntimeWorkspace = true; # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { - "." = mkSharedHash "sha256-h4QW7C8huONRRAoB7S0mN4+DCzbvLd/onGyz7G9O83I="; + "." = mkSharedHash "sha256-NZFaXovwd369eJo1aNnvcTlsQC/4SMFbZJpgvPR34LQ="; }; nativeNodePackages = opentuiCoreNative.packages; inherit gitRev commitTs dirty; diff --git a/packages/@overeng/notion-cli/package.json b/packages/@overeng/notion-cli/package.json index 7c84e82aa9..d62b24b37e 100644 --- a/packages/@overeng/notion-cli/package.json +++ b/packages/@overeng/notion-cli/package.json @@ -22,17 +22,10 @@ "storybook:build": "storybook build" }, "dependencies": { - "@effect-atom/atom": "0.5.3", - "@effect-atom/atom-react": "0.5.0", - "@effect/cli": "0.75.2", - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/atom-react": "4.0.0-beta.102", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@opentui/core": "0.4.1", "@opentui/react": "0.4.1", "@overeng/effect-path": "workspace:^", @@ -48,23 +41,21 @@ "@storybook/react": "10.4.6", "@types/react": "19.2.17", "@types/react-reconciler": "0.33.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-dom": "19.2.7", "react-reconciler": "0.33.0", "vitest": "4.1.9" }, "devDependencies": { - "@effect/cli": "0.75.2", - "@effect/sql": "0.51.1", - "@effect/typeclass": "0.40.0", - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@storybook/react": "10.4.6", "@storybook/react-vite": "10.4.6", "@types/node": "26.0.0", "@types/react": "19.2.17", "@vitejs/plugin-react": "6.0.2", + "effect": "4.0.0-beta.102", "storybook": "10.4.6", "typescript": "6.0.3", "vite": "8.0.16", @@ -76,25 +67,16 @@ } }, "peerDependencies": { - "@effect-atom/atom": "^0.5.3", - "@effect-atom/atom-react": "^0.5.0", - "@effect/cli": "^0.75.2", - "@effect/cluster": "^0.59.0", - "@effect/experimental": "^0.60.0", - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/rpc": "^0.75.1", - "@effect/sql": "^0.51.1", - "@effect/typeclass": "^0.40.0", - "@effect/workflow": "^0.18.2", + "@effect/atom-react": "^4.0.0-beta.102", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@opentui/core": "^0.4.1", "@opentui/react": "^0.4.1", "@playwright/test": "^1.61.0", "@storybook/react": "^10.4.6", "@types/react": "^19.2.17", "@types/react-reconciler": "^0.33.0", - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "react": "^19.2.7", "react-dom": "^19.2.7", "react-reconciler": "^0.33.0" diff --git a/packages/@overeng/notion-cli/package.json.genie.ts b/packages/@overeng/notion-cli/package.json.genie.ts index e9cfc19949..8126d750ee 100644 --- a/packages/@overeng/notion-cli/package.json.genie.ts +++ b/packages/@overeng/notion-cli/package.json.genie.ts @@ -18,7 +18,7 @@ import utilsDevPkg from '../utils-dev/package.json.genie.ts' import utilsPkg from '../utils/package.json.genie.ts' /** Effect packages not already in @overeng/utils or @overeng/tui-react */ -const ownPeerDepNames = ['@effect/cli', '@effect/sql', '@effect/typeclass'] as const +const ownPeerDepNames = ['effect'] as const const runtimeDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/notion-cli' }), dependencies: { @@ -37,19 +37,7 @@ const runtimeDeps = catalog.compose({ devDependencies: { workspace: [utilsDevPkg], external: { - ...catalog.pick( - ...ownPeerDepNames, - '@effect/vitest', - '@storybook/react', - '@storybook/react-vite', - '@types/node', - '@types/react', - '@vitejs/plugin-react', - 'storybook', - 'typescript', - 'vite', - 'vitest', - ), + ...catalog.pick(...ownPeerDepNames, '@effect/vitest', '@storybook/react', '@storybook/react-vite', '@types/node', '@types/react', '@vitejs/plugin-react', 'storybook', 'typescript', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/notion-cli/src/cli-command.unit.test.ts b/packages/@overeng/notion-cli/src/cli-command.unit.test.ts index 9cca3c75f4..5346187139 100644 --- a/packages/@overeng/notion-cli/src/cli-command.unit.test.ts +++ b/packages/@overeng/notion-cli/src/cli-command.unit.test.ts @@ -1,4 +1,4 @@ -import { Command } from '@effect/cli' +import { Command } from 'effect/unstable/cli' import { Effect } from 'effect' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-cli/src/cli.ts b/packages/@overeng/notion-cli/src/cli.ts index a8f76ef19a..1dfa365af2 100644 --- a/packages/@overeng/notion-cli/src/cli.ts +++ b/packages/@overeng/notion-cli/src/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun -import { Command } from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { Command } from 'effect/unstable/cli' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Cause, Effect, type Exit, Layer, Option, Schema } from 'effect' import { editorExitCode } from '@overeng/notion-md' @@ -136,7 +136,7 @@ const runRootCli = async (argv: ReadonlyArray) => { return Effect.void } - return Option.match(Cause.failureOption(cause), { + return Option.match(Cause.findErrorOption(cause), { onNone: () => Effect.logError(cause), onSome: (error) => { const unknownError: unknown = error @@ -150,7 +150,7 @@ const runRootCli = async (argv: ReadonlyArray) => { Effect.provideService(CliVersion, { name: 'notion', version }), Effect.provide( Layer.mergeAll( - NodeContext.layer, + NodeServices.layer, CurrentWorkingDirectory.live, withTelemetry({ identity, shape: 'cli', endpoint }), ), diff --git a/packages/@overeng/notion-cli/src/codegen.ts b/packages/@overeng/notion-cli/src/codegen.ts index a5cafaa8cf..b8e78b67ec 100644 --- a/packages/@overeng/notion-cli/src/codegen.ts +++ b/packages/@overeng/notion-cli/src/codegen.ts @@ -474,7 +474,7 @@ const generatePropertyField = (options: { ) const metaSuffix = schemaMeta === true - ? `.annotations({ [notionPropertyMeta]: ${formatMetaValue(buildPropertyMeta(property))} })` + ? `.annotate({ [notionPropertyMeta]: ${formatMetaValue(buildPropertyMeta(property))} })` : '' if (transform === 'asName') { return `${base}.pipe(NotionSchema.asName)${metaSuffix}` @@ -493,7 +493,7 @@ const generatePropertyField = (options: { ) const metaSuffix = schemaMeta === true - ? `.annotations({ [notionPropertyMeta]: ${formatMetaValue(buildPropertyMeta(property))} })` + ? `.annotate({ [notionPropertyMeta]: ${formatMetaValue(buildPropertyMeta(property))} })` : '' if (transform === 'asName') { return `${base}.pipe(NotionSchema.asName)${metaSuffix}` @@ -512,7 +512,7 @@ const generatePropertyField = (options: { ) const metaSuffix = schemaMeta === true - ? `.annotations({ [notionPropertyMeta]: ${formatMetaValue(buildPropertyMeta(property))} })` + ? `.annotate({ [notionPropertyMeta]: ${formatMetaValue(buildPropertyMeta(property))} })` : '' if (transform === 'asNames') { return `${base}.pipe(NotionSchema.asNames)${metaSuffix}` @@ -527,7 +527,7 @@ const generatePropertyField = (options: { const value = `NotionSchema.${transformKey}` return schemaMeta === true - ? `${value}.annotations({ [notionPropertyMeta]: ${formatMetaValue(buildPropertyMeta(property))} })` + ? `${value}.annotate({ [notionPropertyMeta]: ${formatMetaValue(buildPropertyMeta(property))} })` : value } @@ -583,7 +583,7 @@ const generateTypedOptions = (opts: { property.description !== undefined ? `,\n description: ${toSingleQuotedStringLiteral(property.description)},` : '' - const code = `export const ${typeName} = Schema.Literal(${literals}).annotations({ + const code = `export const ${typeName} = Schema.Literal(${literals}).annotate({ identifier: '${typeName}'${descPart} }) export type ${typeName} = typeof ${typeName}.Type` @@ -819,7 +819,7 @@ export function generateSchemaCode(opts: GenerateSchemaCodeOptions): string { lines.push(` */`) lines.push(`export const ${pascalName}PageProperties = Schema.Struct({`) lines.push(readPropertyFields) - lines.push(`}).annotations({`) + lines.push(`}).annotate({`) lines.push(` identifier: '${pascalName}PageProperties',`) lines.push(` description: 'Read schema for ${dbInfo.name} database pages',`) lines.push(`})`) @@ -855,7 +855,7 @@ export function generateSchemaCode(opts: GenerateSchemaCodeOptions): string { lines.push(` */`) lines.push(`export const ${pascalName}PageWrite = Schema.Struct({`) lines.push(writePropertyFields) - lines.push(`}).annotations({`) + lines.push(`}).annotate({`) lines.push(` identifier: '${pascalName}PageWrite',`) lines.push(` description: 'Write schema for ${dbInfo.name} database pages',`) lines.push(`})`) diff --git a/packages/@overeng/notion-cli/src/codegen.unit.test.ts b/packages/@overeng/notion-cli/src/codegen.unit.test.ts index b4fb6fc390..fea95a043f 100644 --- a/packages/@overeng/notion-cli/src/codegen.unit.test.ts +++ b/packages/@overeng/notion-cli/src/codegen.unit.test.ts @@ -157,11 +157,11 @@ describe('codegen', () => { * Schema for reading pages from the "Test Database" database. */ export const TestDatabasePageProperties = Schema.Struct({ - Name: NotionSchema.title.annotations({ [notionPropertyMeta]: { _tag: 'title', id: 'title-prop', name: 'Name', description: null } }), - Description: NotionSchema.richTextOption.annotations({ [notionPropertyMeta]: { _tag: 'rich_text', id: 'text-prop', name: 'Description', description: null } }), - Count: NotionSchema.numberOption.annotations({ [notionPropertyMeta]: { _tag: 'number', id: 'num-prop', name: 'Count', description: null, number: { format: 'number' } } }), - Done: NotionSchema.checkbox.annotations({ [notionPropertyMeta]: { _tag: 'checkbox', id: 'check-prop', name: 'Done', description: null } }), - }).annotations({ + Name: NotionSchema.title.annotate({ [notionPropertyMeta]: { _tag: 'title', id: 'title-prop', name: 'Name', description: null } }), + Description: NotionSchema.richTextOption.annotate({ [notionPropertyMeta]: { _tag: 'rich_text', id: 'text-prop', name: 'Description', description: null } }), + Count: NotionSchema.numberOption.annotate({ [notionPropertyMeta]: { _tag: 'number', id: 'num-prop', name: 'Count', description: null, number: { format: 'number' } } }), + Done: NotionSchema.checkbox.annotate({ [notionPropertyMeta]: { _tag: 'checkbox', id: 'check-prop', name: 'Done', description: null } }), + }).annotate({ identifier: 'TestDatabasePageProperties', description: 'Read schema for Test Database database pages', }) @@ -217,8 +217,8 @@ describe('codegen', () => { * Schema for reading pages from the "Test Database" database. */ export const TestDatabasePageProperties = Schema.Struct({ - Name: NotionSchema.title.annotations({ [notionPropertyMeta]: { _tag: 'title', id: 'title-prop', name: 'Name', description: null } }), - }).annotations({ + Name: NotionSchema.title.annotate({ [notionPropertyMeta]: { _tag: 'title', id: 'title-prop', name: 'Name', description: null } }), + }).annotate({ identifier: 'TestDatabasePageProperties', description: 'Read schema for Test Database database pages', }) @@ -272,10 +272,10 @@ describe('codegen', () => { * Schema for reading pages from the "Test" database. */ export const TestPageProperties = Schema.Struct({ - 'Due Date': NotionSchema.dateOption.annotations({ [notionPropertyMeta]: { _tag: 'date', id: 'prop1', name: 'Due Date', description: null } }), - 'Project Name': NotionSchema.title.annotations({ [notionPropertyMeta]: { _tag: 'title', id: 'prop2', name: 'Project Name', description: null } }), - 'User\\'s Choice': NotionSchema.select().annotations({ [notionPropertyMeta]: { _tag: 'select', id: 'prop3', name: 'User\\'s Choice', description: null, select: { options: [] } } }), - }).annotations({ + 'Due Date': NotionSchema.dateOption.annotate({ [notionPropertyMeta]: { _tag: 'date', id: 'prop1', name: 'Due Date', description: null } }), + 'Project Name': NotionSchema.title.annotate({ [notionPropertyMeta]: { _tag: 'title', id: 'prop2', name: 'Project Name', description: null } }), + 'User\\'s Choice': NotionSchema.select().annotate({ [notionPropertyMeta]: { _tag: 'select', id: 'prop3', name: 'User\\'s Choice', description: null, select: { options: [] } } }), + }).annotate({ identifier: 'TestPageProperties', description: 'Read schema for Test database pages', }) @@ -336,10 +336,10 @@ describe('codegen', () => { * Schema for reading pages from the "Test" database. */ export const TestPageProperties = Schema.Struct({ - Status: NotionSchema.select().pipe(NotionSchema.asNullable).annotations({ [notionPropertyMeta]: { _tag: 'select', id: 'prop1', name: 'Status', description: null, select: { options: [] } } }), - Tags: NotionSchema.multiSelect().annotations({ [notionPropertyMeta]: { _tag: 'multi_select', id: 'prop2', name: 'Tags', description: null, multi_select: { options: [] } } }), - Website: NotionSchema.urlString.annotations({ [notionPropertyMeta]: { _tag: 'url', id: 'prop3', name: 'Website', description: null } }), - }).annotations({ + Status: NotionSchema.select().pipe(NotionSchema.asNullable).annotate({ [notionPropertyMeta]: { _tag: 'select', id: 'prop1', name: 'Status', description: null, select: { options: [] } } }), + Tags: NotionSchema.multiSelect().annotate({ [notionPropertyMeta]: { _tag: 'multi_select', id: 'prop2', name: 'Tags', description: null, multi_select: { options: [] } } }), + Website: NotionSchema.urlString.annotate({ [notionPropertyMeta]: { _tag: 'url', id: 'prop3', name: 'Website', description: null } }), + }).annotate({ identifier: 'TestPageProperties', description: 'Read schema for Test database pages', }) @@ -441,8 +441,8 @@ describe('codegen', () => { */ export const TestPageProperties = Schema.Struct({ /** Current task status */ - Status: NotionSchema.select().annotations({ [notionPropertyMeta]: { _tag: 'select', id: 'prop1', name: 'Status', description: 'Current task status', select: { options: [] } } }), - }).annotations({ + Status: NotionSchema.select().annotate({ [notionPropertyMeta]: { _tag: 'select', id: 'prop1', name: 'Status', description: 'Current task status', select: { options: [] } } }), + }).annotate({ identifier: 'TestPageProperties', description: 'Read schema for Test database pages', }) @@ -695,10 +695,10 @@ describe('codegen', () => { * Schema for reading pages from the "Test" database. */ export const TestPageProperties = Schema.Struct({ - Name: NotionSchema.title.annotations({ [notionPropertyMeta]: { _tag: 'title', id: 'prop1', name: 'Name', description: null } }), - Status: NotionSchema.select().annotations({ [notionPropertyMeta]: { _tag: 'select', id: 'prop2', name: 'Status', description: null, select: { options: [] } } }), - CreatedAt: NotionSchema.createdTimeDate.annotations({ [notionPropertyMeta]: { _tag: 'created_time', id: 'prop3', name: 'CreatedAt', description: null } }), - }).annotations({ + Name: NotionSchema.title.annotate({ [notionPropertyMeta]: { _tag: 'title', id: 'prop1', name: 'Name', description: null } }), + Status: NotionSchema.select().annotate({ [notionPropertyMeta]: { _tag: 'select', id: 'prop2', name: 'Status', description: null, select: { options: [] } } }), + CreatedAt: NotionSchema.createdTimeDate.annotate({ [notionPropertyMeta]: { _tag: 'created_time', id: 'prop3', name: 'CreatedAt', description: null } }), + }).annotate({ identifier: 'TestPageProperties', description: 'Read schema for Test database pages', }) @@ -726,7 +726,7 @@ describe('codegen', () => { export const TestPageWrite = Schema.Struct({ Name: NotionSchema.titleWriteFromString, Status: NotionSchema.selectWriteFromName, - }).annotations({ + }).annotate({ identifier: 'TestPageWrite', description: 'Write schema for Test database pages', }) @@ -812,13 +812,13 @@ describe('codegen', () => { // ----------------------------------------------------------------------------- /** Options for "Status" property */ - export const TestStatusOption = Schema.Literal('Done', 'In Progress', 'Not Started').annotations({ + export const TestStatusOption = Schema.Literals(['Done', 'In Progress', 'Not Started']).annotate({ identifier: 'TestStatusOption' }) export type TestStatusOption = typeof TestStatusOption.Type /** Options for "Priority" property */ - export const TestPriorityOption = Schema.Literal('High', 'Low').annotations({ + export const TestPriorityOption = Schema.Literals(['High', 'Low']).annotate({ identifier: 'TestPriorityOption' }) export type TestPriorityOption = typeof TestPriorityOption.Type @@ -832,9 +832,9 @@ describe('codegen', () => { * Schema for reading pages from the "Test" database. */ export const TestPageProperties = Schema.Struct({ - Status: NotionSchema.select(TestStatusOption).annotations({ [notionPropertyMeta]: { _tag: 'select', id: 'prop1', name: 'Status', description: null, select: { options: [{ id: '1', name: 'Not Started', color: 'gray', description: null }, { id: '2', name: 'In Progress', color: 'blue', description: null }, { id: '3', name: 'Done', color: 'green', description: null }] } } }), - Priority: NotionSchema.multiSelect(TestPriorityOption).annotations({ [notionPropertyMeta]: { _tag: 'multi_select', id: 'prop2', name: 'Priority', description: null, multi_select: { options: [{ id: '1', name: 'High', color: 'red', description: null }, { id: '2', name: 'Low', color: 'gray', description: null }] } } }), - }).annotations({ + Status: NotionSchema.select(TestStatusOption).annotate({ [notionPropertyMeta]: { _tag: 'select', id: 'prop1', name: 'Status', description: null, select: { options: [{ id: '1', name: 'Not Started', color: 'gray', description: null }, { id: '2', name: 'In Progress', color: 'blue', description: null }, { id: '3', name: 'Done', color: 'green', description: null }] } } }), + Priority: NotionSchema.multiSelect(TestPriorityOption).annotate({ [notionPropertyMeta]: { _tag: 'multi_select', id: 'prop2', name: 'Priority', description: null, multi_select: { options: [{ id: '1', name: 'High', color: 'red', description: null }, { id: '2', name: 'Low', color: 'gray', description: null }] } } }), + }).annotate({ identifier: 'TestPageProperties', description: 'Read schema for Test database pages', }) @@ -935,8 +935,8 @@ describe('codegen', () => { * Schema for reading pages from the "Test" database. */ export const TestPageProperties = Schema.Struct({ - Name: NotionSchema.title.annotations({ [notionPropertyMeta]: { _tag: 'title', id: 'prop1', name: 'Name', description: null } }), - }).annotations({ + Name: NotionSchema.title.annotate({ [notionPropertyMeta]: { _tag: 'title', id: 'prop1', name: 'Name', description: null } }), + }).annotate({ identifier: 'TestPageProperties', description: 'Read schema for Test database pages', }) @@ -963,7 +963,7 @@ describe('codegen', () => { */ export const TestPageWrite = Schema.Struct({ Name: NotionSchema.titleWriteFromString, - }).annotations({ + }).annotate({ identifier: 'TestPageWrite', description: 'Write schema for Test database pages', }) diff --git a/packages/@overeng/notion-cli/src/commands/db/mod.ts b/packages/@overeng/notion-cli/src/commands/db/mod.ts index e3f624049e..e54340aeb1 100644 --- a/packages/@overeng/notion-cli/src/commands/db/mod.ts +++ b/packages/@overeng/notion-cli/src/commands/db/mod.ts @@ -2,9 +2,9 @@ * Database subcommand - database information plus Node-backed replica commands. */ -import { Args, Command } from '@effect/cli' -import { FetchHttpClient } from '@effect/platform' import { Effect, Layer } from 'effect' +import { Argument as Args, Command } from 'effect/unstable/cli' +import { FetchHttpClient } from 'effect/unstable/http' import React from 'react' import { @@ -19,14 +19,14 @@ import { InfoView } from '../../renderers/InfoOutput/view.tsx' /** Re-export internal types for TypeScript declaration emit */ export type { Cause, Channel, Sink, Stream } from 'effect' export type { NodeInspectSymbol } from 'effect/Inspectable' -export type { PlatformError } from '@effect/platform/Error' +export type { PlatformError } from 'effect/Error' import { NotionConfig, NotionDatabases, NotionDataSources } from '@overeng/notion-effect-client' import { run } from '@overeng/tui-react' import { resolveNotionToken, tokenOption } from '../shared.ts' -const databaseIdArg = Args.text({ name: 'database-id' }).pipe( +const databaseIdArg = Args.string('database-id').pipe( Args.withDescription('The Notion database ID to operate on'), ) diff --git a/packages/@overeng/notion-cli/src/commands/schema/mod.ts b/packages/@overeng/notion-cli/src/commands/schema/mod.ts index b71f09f5de..7dce61c268 100644 --- a/packages/@overeng/notion-cli/src/commands/schema/mod.ts +++ b/packages/@overeng/notion-cli/src/commands/schema/mod.ts @@ -5,9 +5,10 @@ import { basename } from 'node:path' import { fileURLToPath } from 'node:url' -import { Args, Command, Options } from '@effect/cli' -import { FetchHttpClient, FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect, Layer, Option, Schema } from 'effect' +import { Argument as Args, Command, Flag as Options } from 'effect/unstable/cli' +import { FetchHttpClient } from 'effect/unstable/http' import React from 'react' import { EffectPath } from '@overeng/effect-path' @@ -30,7 +31,7 @@ import { IntrospectView } from '../../renderers/IntrospectOutput/view.tsx' import { resolveNotionToken, tokenOption } from '../shared.ts' /** Re-export internal types for TypeScript declaration emit */ -export type { PlatformError } from '@effect/platform/Error' +export type { PlatformError } from 'effect/Error' import { type GenerateOptions, generateApiCode, generateSchemaCode } from '../../codegen.ts' import { loadConfig } from '../../config.ts' @@ -45,7 +46,7 @@ export { resolveNotionToken, tokenOption } from '../shared.ts' // ----------------------------------------------------------------------------- /** Error thrown when a generated schema file cannot be parsed for drift detection */ -export class GeneratedSchemaFileParseError extends Schema.TaggedError()( +export class GeneratedSchemaFileParseError extends Schema.TaggedErrorClass()( 'GeneratedSchemaFileParseError', { file: Schema.String, @@ -54,7 +55,7 @@ export class GeneratedSchemaFileParseError extends Schema.TaggedError()( +export class SchemaDriftDetectedError extends Schema.TaggedErrorClass()( 'SchemaDriftDetectedError', { databaseId: Schema.String, @@ -73,7 +74,9 @@ const getGeneratorVersion = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const pkgJsonPath = fileURLToPath(new URL('../../../package.json', import.meta.url)) const content = yield* fs.readFileString(pkgJsonPath) - const pkg = yield* Schema.decodeUnknown(Schema.parseJson(GeneratorPackageJsonSchema))(content) + const pkg = yield* Schema.decodeUnknown(Schema.fromJsonString(GeneratorPackageJsonSchema))( + content, + ) return pkg.version }).pipe(Effect.orElseSucceed(() => 'unknown')) @@ -81,7 +84,7 @@ const getGeneratorVersion = Effect.gen(function* () { // Generate Command // ----------------------------------------------------------------------------- -const generateDatabaseIdArg = Args.text({ name: 'database-id' }).pipe( +const generateDatabaseIdArg = Args.string('database-id').pipe( Args.withDescription('The Notion database ID to generate schema from'), ) @@ -103,7 +106,7 @@ const tuiOutputModeOption = Options.choice('output-mode', OUTPUT_MODE_VALUES).pi Options.withDefault('auto' as (typeof OUTPUT_MODE_VALUES)[number]), ) -const nameOption = Options.text('name').pipe( +const nameOption = Options.string('name').pipe( Options.withAlias('n'), Options.withDescription('Name for the generated schema (defaults to database title)'), Options.optional, @@ -317,7 +320,7 @@ export const generateCommand = Command.make( // Introspect Command // ----------------------------------------------------------------------------- -const introspectDatabaseIdArg = Args.text({ name: 'database-id' }).pipe( +const introspectDatabaseIdArg = Args.string('database-id').pipe( Args.withDescription('The Notion database ID to introspect'), ) @@ -546,7 +549,7 @@ const generateFromConfigCommand = Command.make( // Diff Command // ----------------------------------------------------------------------------- -const diffDatabaseIdArg = Args.text({ name: 'database-id' }).pipe( +const diffDatabaseIdArg = Args.string('database-id').pipe( Args.withDescription('The Notion database ID to compare against'), ) diff --git a/packages/@overeng/notion-cli/src/commands/schema/mod.unit.test.ts b/packages/@overeng/notion-cli/src/commands/schema/mod.unit.test.ts index 0ad4bf6014..6dd0af15c6 100644 --- a/packages/@overeng/notion-cli/src/commands/schema/mod.unit.test.ts +++ b/packages/@overeng/notion-cli/src/commands/schema/mod.unit.test.ts @@ -1,5 +1,5 @@ -import { Command } from '@effect/cli' -import { NodeContext } from '@effect/platform-node' +import { Command } from 'effect/unstable/cli' +import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect } from 'effect' import { expect } from 'vitest' @@ -39,6 +39,6 @@ describe('schema generate option resolution', () => { expect(captured!.output).toBe('schema.gen.ts') // The TUI render mode falls back to its default rather than swallowing `-o`. expect(captured!.tuiOutput).toBe('auto') - }).pipe(Effect.provide(NodeContext.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ) }) diff --git a/packages/@overeng/notion-cli/src/commands/shared.ts b/packages/@overeng/notion-cli/src/commands/shared.ts index 58d3e3d868..96d856fbd7 100644 --- a/packages/@overeng/notion-cli/src/commands/shared.ts +++ b/packages/@overeng/notion-cli/src/commands/shared.ts @@ -1,5 +1,5 @@ -import { Options } from '@effect/cli' import { Effect, Option, Redacted } from 'effect' +import { Flag as Options } from 'effect/unstable/cli' import { resolveNotionToken as resolveNotionTokenFromEnv } from '@overeng/notion-effect-client' @@ -10,7 +10,7 @@ export const resolveNotionToken = (token: Option.Option) => : resolveNotionTokenFromEnv() /** CLI option for providing a Notion API token (defaults to `NOTION_API_TOKEN`). */ -export const tokenOption = Options.text('token').pipe( +export const tokenOption = Options.string('token').pipe( Options.withAlias('t'), Options.withDescription('Notion API token (defaults to NOTION_API_TOKEN env var)'), Options.optional, diff --git a/packages/@overeng/notion-cli/src/config.ts b/packages/@overeng/notion-cli/src/config.ts index 1fc1e246cd..0b8567b6f0 100644 --- a/packages/@overeng/notion-cli/src/config.ts +++ b/packages/@overeng/notion-cli/src/config.ts @@ -1,4 +1,4 @@ -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect, Schema } from 'effect' import { EffectPath, type AbsoluteDirPath, type AbsoluteFilePath } from '@overeng/effect-path' @@ -53,7 +53,7 @@ export interface ResolvedConfig { // ----------------------------------------------------------------------------- /** Error thrown when no config file is found in the search path */ -export class ConfigNotFoundError extends Schema.TaggedError()( +export class ConfigNotFoundError extends Schema.TaggedErrorClass()( 'ConfigNotFoundError', { message: Schema.String, @@ -63,7 +63,7 @@ export class ConfigNotFoundError extends Schema.TaggedError ) {} /** Error thrown when specified config file path doesn't exist */ -export class ConfigFileNotFoundError extends Schema.TaggedError()( +export class ConfigFileNotFoundError extends Schema.TaggedErrorClass()( 'ConfigFileNotFoundError', { message: Schema.String, @@ -72,18 +72,21 @@ export class ConfigFileNotFoundError extends Schema.TaggedError()('ConfigReadError', { +export class ConfigReadError extends Schema.TaggedErrorClass()('ConfigReadError', { message: Schema.String, path: Schema.String, - cause: Schema.Defect, + cause: Schema.Defect(), }) {} /** Error thrown when config file has invalid structure */ -export class ConfigParseError extends Schema.TaggedError()('ConfigParseError', { - message: Schema.String, - path: Schema.String, - cause: Schema.Defect, -}) {} +export class ConfigParseError extends Schema.TaggedErrorClass()( + 'ConfigParseError', + { + message: Schema.String, + path: Schema.String, + cause: Schema.Defect(), + }, +) {} /** Union of all configuration-related errors. */ export type ConfigError = diff --git a/packages/@overeng/notion-cli/src/integration.integration.test.ts b/packages/@overeng/notion-cli/src/integration.integration.test.ts index 998e398499..4ca4635c4d 100644 --- a/packages/@overeng/notion-cli/src/integration.integration.test.ts +++ b/packages/@overeng/notion-cli/src/integration.integration.test.ts @@ -111,13 +111,13 @@ describe('integration', () => { ) // Verify usage is correct - expect(code).toContain('Name: NotionSchema.title.annotations({ [notionPropertyMeta]') + expect(code).toContain('Name: NotionSchema.title.annotate({ [notionPropertyMeta]') expect(code).toContain( - 'Description: NotionSchema.richTextOption.annotations({ [notionPropertyMeta]', + 'Description: NotionSchema.richTextOption.annotate({ [notionPropertyMeta]', ) - expect(code).toContain('Count: NotionSchema.numberOption.annotations({ [notionPropertyMeta]') - expect(code).toContain('Done: NotionSchema.checkbox.annotations({ [notionPropertyMeta]') - expect(code).toContain('Status: NotionSchema.select().annotations({ [notionPropertyMeta]') + expect(code).toContain('Count: NotionSchema.numberOption.annotate({ [notionPropertyMeta]') + expect(code).toContain('Done: NotionSchema.checkbox.annotate({ [notionPropertyMeta]') + expect(code).toContain('Status: NotionSchema.select().annotate({ [notionPropertyMeta]') }) it('should generate schemas that can decode actual Notion API responses', () => { diff --git a/packages/@overeng/notion-cli/src/output.ts b/packages/@overeng/notion-cli/src/output.ts index 71b8a48ca7..13cb2ff72e 100644 --- a/packages/@overeng/notion-cli/src/output.ts +++ b/packages/@overeng/notion-cli/src/output.ts @@ -1,5 +1,5 @@ -import { FileSystem } from '@effect/platform' -import type * as CommandExecutor from '@effect/platform/CommandExecutor' +import { FileSystem } from 'effect/FileSystem' +import type * as CommandExecutor from 'effect/unstable/process/CommandExecutor' import { Data, Effect } from 'effect' import { EffectPath, type AbsoluteFilePath } from '@overeng/effect-path' diff --git a/packages/@overeng/notion-cli/src/output.unit.test.ts b/packages/@overeng/notion-cli/src/output.unit.test.ts index 8f15af87fc..f972d1df1c 100644 --- a/packages/@overeng/notion-cli/src/output.unit.test.ts +++ b/packages/@overeng/notion-cli/src/output.unit.test.ts @@ -1,5 +1,5 @@ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { Effect } from 'effect' import { expect } from 'vitest' @@ -9,7 +9,7 @@ import { Vitest } from '@overeng/utils-dev/node-vitest' import { writeSchemaToFile } from './output.ts' /** Layer providing NodeContext for file system operations */ -const TestLayer = NodeContext.layer +const TestLayer = NodeServices.layer Vitest.describe('output', () => { Vitest.describe('writeSchemaToFile', () => { diff --git a/packages/@overeng/notion-cli/src/renderers/DiffOutput/schema.ts b/packages/@overeng/notion-cli/src/renderers/DiffOutput/schema.ts index 9bbe38ae99..5b819010a3 100644 --- a/packages/@overeng/notion-cli/src/renderers/DiffOutput/schema.ts +++ b/packages/@overeng/notion-cli/src/renderers/DiffOutput/schema.ts @@ -2,7 +2,7 @@ import { Schema } from 'effect' const PropertyChange = Schema.Struct({ name: Schema.String, - type: Schema.Literal('added', 'removed', 'type_changed'), + type: Schema.Literals(['added', 'removed', 'type_changed']), liveType: Schema.optional(Schema.String), liveTransform: Schema.optional(Schema.String), generatedTransformKey: Schema.optional(Schema.String), @@ -15,7 +15,7 @@ const OptionsChange = Schema.Struct({ }) /** Schema for the diff command's UI state (Loading → Success/NoDifferences/Error). */ -export const DiffState = Schema.Union( +export const DiffState = Schema.Union([ Schema.TaggedStruct('Loading', {}), Schema.TaggedStruct('Success', { databaseId: Schema.String, @@ -31,12 +31,12 @@ export const DiffState = Schema.Union( Schema.TaggedStruct('Error', { message: Schema.String, }), -) +]) export type DiffState = typeof DiffState.Type /** Actions dispatched by the diff command to report schema comparison results. */ -export const DiffAction = Schema.Union( +export const DiffAction = Schema.Union([ Schema.TaggedStruct('SetResult', { databaseId: Schema.String, filePath: Schema.String, @@ -49,7 +49,7 @@ export const DiffAction = Schema.Union( filePath: Schema.String, }), Schema.TaggedStruct('SetError', { message: Schema.String }), -) +]) export type DiffAction = typeof DiffAction.Type diff --git a/packages/@overeng/notion-cli/src/renderers/DiffOutput/view.tsx b/packages/@overeng/notion-cli/src/renderers/DiffOutput/view.tsx index 220862e1ca..2f5fba9ade 100644 --- a/packages/@overeng/notion-cli/src/renderers/DiffOutput/view.tsx +++ b/packages/@overeng/notion-cli/src/renderers/DiffOutput/view.tsx @@ -1,4 +1,4 @@ -import { type Atom } from '@effect-atom/atom' +import { type Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/notion-cli/src/renderers/GenerateConfigOutput/schema.ts b/packages/@overeng/notion-cli/src/renderers/GenerateConfigOutput/schema.ts index 9fa88a3366..070bb7f5f7 100644 --- a/packages/@overeng/notion-cli/src/renderers/GenerateConfigOutput/schema.ts +++ b/packages/@overeng/notion-cli/src/renderers/GenerateConfigOutput/schema.ts @@ -3,12 +3,12 @@ import { Schema } from 'effect' const DatabaseEntry = Schema.Struct({ id: Schema.String, name: Schema.String, - status: Schema.Literal('pending', 'introspecting', 'generating', 'writing', 'done', 'error'), + status: Schema.Literals(['pending', 'introspecting', 'generating', 'writing', 'done', 'error']), outputPath: Schema.optional(Schema.String), }) /** Schema for the generate-config command's UI state (Loading → Running → Done/Error). */ -export const GenerateConfigState = Schema.Union( +export const GenerateConfigState = Schema.Union([ Schema.TaggedStruct('Loading', { configPath: Schema.String, }), @@ -23,12 +23,12 @@ export const GenerateConfigState = Schema.Union( Schema.TaggedStruct('Error', { message: Schema.String, }), -) +]) export type GenerateConfigState = typeof GenerateConfigState.Type /** Actions dispatched during config-based generation to update database progress and completion status. */ -export const GenerateConfigAction = Schema.Union( +export const GenerateConfigAction = Schema.Union([ Schema.TaggedStruct('SetConfig', { configPath: Schema.String, databases: Schema.Array( @@ -41,7 +41,7 @@ export const GenerateConfigAction = Schema.Union( }), Schema.TaggedStruct('UpdateDatabase', { id: Schema.String, - status: Schema.Literal('pending', 'introspecting', 'generating', 'writing', 'done', 'error'), + status: Schema.Literals(['pending', 'introspecting', 'generating', 'writing', 'done', 'error']), name: Schema.optional(Schema.String), }), Schema.TaggedStruct('SetDone', { @@ -50,7 +50,7 @@ export const GenerateConfigAction = Schema.Union( Schema.TaggedStruct('SetError', { message: Schema.String, }), -) +]) export type GenerateConfigAction = typeof GenerateConfigAction.Type diff --git a/packages/@overeng/notion-cli/src/renderers/GenerateConfigOutput/view.tsx b/packages/@overeng/notion-cli/src/renderers/GenerateConfigOutput/view.tsx index 215277d46f..3e59d73227 100644 --- a/packages/@overeng/notion-cli/src/renderers/GenerateConfigOutput/view.tsx +++ b/packages/@overeng/notion-cli/src/renderers/GenerateConfigOutput/view.tsx @@ -1,4 +1,4 @@ -import { type Atom } from '@effect-atom/atom' +import { type Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/notion-cli/src/renderers/GenerateOutput.stories.tsx b/packages/@overeng/notion-cli/src/renderers/GenerateOutput.stories.tsx index 16e17a29cb..218b0148fd 100644 --- a/packages/@overeng/notion-cli/src/renderers/GenerateOutput.stories.tsx +++ b/packages/@overeng/notion-cli/src/renderers/GenerateOutput.stories.tsx @@ -86,7 +86,7 @@ const createDryRunState = (): GenerateState => ({ export const TasksDB = Schema.Struct({ id: Schema.String, title: Schema.String, - status: Schema.Literal("todo", "in-progress", "done"), + status: Schema.Literals(["todo", "in-progress", "done"]), priority: Schema.optional(Schema.Number), assignee: Schema.optional(Schema.String), createdAt: Schema.Date, @@ -104,7 +104,7 @@ const createDryRunWithApiState = (): GenerateState => ({ export const TasksDB = Schema.Struct({ id: Schema.String, title: Schema.String, - status: Schema.Literal("todo", "in-progress", "done"), + status: Schema.Literals(["todo", "in-progress", "done"]), priority: Schema.optional(Schema.Number), createdAt: Schema.Date, }) diff --git a/packages/@overeng/notion-cli/src/renderers/GenerateOutput/schema.ts b/packages/@overeng/notion-cli/src/renderers/GenerateOutput/schema.ts index 78ffc4b7a7..2f91b558a6 100644 --- a/packages/@overeng/notion-cli/src/renderers/GenerateOutput/schema.ts +++ b/packages/@overeng/notion-cli/src/renderers/GenerateOutput/schema.ts @@ -1,7 +1,7 @@ import { Schema } from 'effect' /** Schema for the single-database generate command's UI state (Introspecting → Generating → Writing → Done/DryRun/Error). */ -export const GenerateState = Schema.Union( +export const GenerateState = Schema.Union([ Schema.TaggedStruct('Introspecting', { databaseId: Schema.String }), Schema.TaggedStruct('Generating', { schemaName: Schema.String }), Schema.TaggedStruct('Writing', { outputPath: Schema.String }), @@ -17,12 +17,12 @@ export const GenerateState = Schema.Union( apiOutputPath: Schema.optional(Schema.String), }), Schema.TaggedStruct('Error', { message: Schema.String }), -) +]) export type GenerateState = typeof GenerateState.Type /** Actions dispatched during single-database generation to transition through pipeline stages. */ -export const GenerateAction = Schema.Union( +export const GenerateAction = Schema.Union([ Schema.TaggedStruct('SetIntrospecting', { databaseId: Schema.String }), Schema.TaggedStruct('SetGenerating', { schemaName: Schema.String }), Schema.TaggedStruct('SetWriting', { outputPath: Schema.String }), @@ -38,7 +38,7 @@ export const GenerateAction = Schema.Union( apiOutputPath: Schema.optional(Schema.String), }), Schema.TaggedStruct('SetError', { message: Schema.String }), -) +]) export type GenerateAction = typeof GenerateAction.Type diff --git a/packages/@overeng/notion-cli/src/renderers/GenerateOutput/view.tsx b/packages/@overeng/notion-cli/src/renderers/GenerateOutput/view.tsx index 00e91d873c..0a117781ab 100644 --- a/packages/@overeng/notion-cli/src/renderers/GenerateOutput/view.tsx +++ b/packages/@overeng/notion-cli/src/renderers/GenerateOutput/view.tsx @@ -1,4 +1,4 @@ -import { type Atom } from '@effect-atom/atom' +import { type Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/notion-cli/src/renderers/InfoOutput/schema.ts b/packages/@overeng/notion-cli/src/renderers/InfoOutput/schema.ts index ed38504dfa..4d84f4408d 100644 --- a/packages/@overeng/notion-cli/src/renderers/InfoOutput/schema.ts +++ b/packages/@overeng/notion-cli/src/renderers/InfoOutput/schema.ts @@ -6,7 +6,7 @@ const PropertyInfo = Schema.Struct({ }) /** Schema for the info command's UI state (Loading → Success/Error). */ -export const InfoState = Schema.Union( +export const InfoState = Schema.Union([ Schema.TaggedStruct('Loading', {}), Schema.TaggedStruct('Success', { dbName: Schema.String, @@ -18,12 +18,12 @@ export const InfoState = Schema.Union( Schema.TaggedStruct('Error', { message: Schema.String, }), -) +]) export type InfoState = typeof InfoState.Type /** Actions dispatched by the info command to report database metadata or errors. */ -export const InfoAction = Schema.Union( +export const InfoAction = Schema.Union([ Schema.TaggedStruct('SetResult', { dbName: Schema.String, dbId: Schema.String, @@ -32,7 +32,7 @@ export const InfoAction = Schema.Union( rowCount: Schema.String, }), Schema.TaggedStruct('SetError', { message: Schema.String }), -) +]) export type InfoAction = typeof InfoAction.Type diff --git a/packages/@overeng/notion-cli/src/renderers/InfoOutput/view.tsx b/packages/@overeng/notion-cli/src/renderers/InfoOutput/view.tsx index 8f7f379f4e..3013ed7b9c 100644 --- a/packages/@overeng/notion-cli/src/renderers/InfoOutput/view.tsx +++ b/packages/@overeng/notion-cli/src/renderers/InfoOutput/view.tsx @@ -1,4 +1,4 @@ -import { type Atom } from '@effect-atom/atom' +import { type Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/notion-cli/src/renderers/IntrospectOutput/schema.ts b/packages/@overeng/notion-cli/src/renderers/IntrospectOutput/schema.ts index 15d6e35e65..023ca06812 100644 --- a/packages/@overeng/notion-cli/src/renderers/IntrospectOutput/schema.ts +++ b/packages/@overeng/notion-cli/src/renderers/IntrospectOutput/schema.ts @@ -9,7 +9,7 @@ const PropertyInfo = Schema.Struct({ }) /** Schema for the introspect command's UI state (Loading → Success/Error). */ -export const IntrospectState = Schema.Union( +export const IntrospectState = Schema.Union([ Schema.TaggedStruct('Loading', {}), Schema.TaggedStruct('Success', { dbName: Schema.String, @@ -20,12 +20,12 @@ export const IntrospectState = Schema.Union( Schema.TaggedStruct('Error', { message: Schema.String, }), -) +]) export type IntrospectState = typeof IntrospectState.Type /** Actions dispatched by the introspect command to report results or errors. */ -export const IntrospectAction = Schema.Union( +export const IntrospectAction = Schema.Union([ Schema.TaggedStruct('SetResult', { dbName: Schema.String, dbId: Schema.String, @@ -33,7 +33,7 @@ export const IntrospectAction = Schema.Union( properties: Schema.Array(PropertyInfo), }), Schema.TaggedStruct('SetError', { message: Schema.String }), -) +]) export type IntrospectAction = typeof IntrospectAction.Type diff --git a/packages/@overeng/notion-cli/src/renderers/IntrospectOutput/view.tsx b/packages/@overeng/notion-cli/src/renderers/IntrospectOutput/view.tsx index 315c7803a2..96c4fc8e53 100644 --- a/packages/@overeng/notion-cli/src/renderers/IntrospectOutput/view.tsx +++ b/packages/@overeng/notion-cli/src/renderers/IntrospectOutput/view.tsx @@ -1,4 +1,4 @@ -import { type Atom } from '@effect-atom/atom' +import { type Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue, useSymbols } from '@overeng/tui-react' diff --git a/packages/@overeng/notion-core/package.json b/packages/@overeng/notion-core/package.json index ab6748971e..3c5d913c32 100644 --- a/packages/@overeng/notion-core/package.json +++ b/packages/@overeng/notion-core/package.json @@ -15,7 +15,7 @@ "devDependencies": { "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, diff --git a/packages/@overeng/notion-datasource-sync/package.json b/packages/@overeng/notion-datasource-sync/package.json index 7f40e86b14..a26745929f 100644 --- a/packages/@overeng/notion-datasource-sync/package.json +++ b/packages/@overeng/notion-datasource-sync/package.json @@ -68,17 +68,10 @@ "react": "19.2.7" }, "devDependencies": { - "@effect-atom/atom": "0.5.3", - "@effect-atom/atom-react": "0.5.0", - "@effect/cli": "0.75.2", - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/atom-react": "4.0.0-beta.102", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@opentui/core": "0.4.1", "@opentui/react": "0.4.1", "@playwright/test": "1.61.0", @@ -86,7 +79,7 @@ "@types/node": "26.0.0", "@types/react": "19.2.17", "@types/react-reconciler": "0.33.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react-dom": "19.2.7", "react-reconciler": "0.33.0", "typescript": "6.0.3", @@ -98,16 +91,10 @@ } }, "peerDependencies": { - "@effect/cli": "^0.75.2", - "@effect/cluster": "^0.59.0", - "@effect/experimental": "^0.60.0", - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/rpc": "^0.75.1", - "@effect/workflow": "^0.18.2", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@playwright/test": "^1.61.0", - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "engines": { "node": ">=24.0.0" diff --git a/packages/@overeng/notion-datasource-sync/package.json.genie.ts b/packages/@overeng/notion-datasource-sync/package.json.genie.ts index 01edcd6e34..82659a17a9 100644 --- a/packages/@overeng/notion-datasource-sync/package.json.genie.ts +++ b/packages/@overeng/notion-datasource-sync/package.json.genie.ts @@ -17,18 +17,7 @@ import otelContractPkg from '../otel-contract/package.json.genie.ts' import tuiReactPkg from '../tui-react/package.json.genie.ts' import utilsPkg from '../utils/package.json.genie.ts' -const peerDepNames = [ - '@effect/cli', - '@effect/cluster', - '@effect/experimental', - '@effect/opentelemetry', - '@effect/platform', - '@effect/platform-node', - '@effect/rpc', - '@effect/workflow', - '@playwright/test', - 'effect', -] as const +const peerDepNames = ['effect', '@effect/opentelemetry', '@effect/platform-node', '@playwright/test'] as const const workspaceDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/notion-datasource-sync' }), @@ -48,22 +37,7 @@ const workspaceDeps = catalog.compose({ }, devDependencies: { external: { - ...catalog.pick( - ...peerDepNames, - '@effect-atom/atom', - '@effect-atom/atom-react', - '@effect/vitest', - '@opentui/core', - '@opentui/react', - '@storybook/react', - '@types/node', - '@types/react', - '@types/react-reconciler', - 'react-dom', - 'react-reconciler', - 'typescript', - 'vitest', - ), + ...catalog.pick(...peerDepNames, '@effect/atom-react', '@effect/vitest', '@opentui/core', '@opentui/react', '@storybook/react', '@types/node', '@types/react', '@types/react-reconciler', 'react-dom', 'react-reconciler', 'typescript', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/notion-datasource-sync/src/body/adapter.unit.test.ts b/packages/@overeng/notion-datasource-sync/src/body/adapter.unit.test.ts index d0d85e497a..e6dac3336c 100644 --- a/packages/@overeng/notion-datasource-sync/src/body/adapter.unit.test.ts +++ b/packages/@overeng/notion-datasource-sync/src/body/adapter.unit.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Chunk, Effect, Layer, Schema, Stream } from 'effect' import { describe, expect, it } from 'vitest' @@ -164,7 +164,7 @@ const runWithNmdStateStore = ( effect: Effect.Effect, ) => Effect.runPromise( - effect.pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)))), + effect.pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)))), ) describe('body adapter contract', () => { diff --git a/packages/@overeng/notion-datasource-sync/src/body/notion-md.ts b/packages/@overeng/notion-datasource-sync/src/body/notion-md.ts index 0113a3f037..f40b8a59c6 100644 --- a/packages/@overeng/notion-datasource-sync/src/body/notion-md.ts +++ b/packages/@overeng/notion-datasource-sync/src/body/notion-md.ts @@ -1,7 +1,7 @@ import { mkdir, rename, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer, Schema, Stream } from 'effect' import type { ContentDescriptor } from '@overeng/content-address' @@ -201,16 +201,19 @@ const provideNotionMdGatewayAndStateStore = ) /** Raised when atomically encoding/writing a JSON sidecar to the workspace fails. */ -class JsonFileWriteError extends Schema.TaggedError()('JsonFileWriteError', { - path: Schema.String, - message: Schema.String, - cause: Schema.Defect, -}) {} +class JsonFileWriteError extends Schema.TaggedErrorClass()( + 'JsonFileWriteError', + { + path: Schema.String, + message: Schema.String, + cause: Schema.Defect(), + }, +) {} /** * Atomically writes a schema-encoded JSON file via a `.tmp` rename. * - * The value is encoded through `Schema.parseJson(schema, { space: 2 })` so the + * The value is encoded through `Schema.fromJsonString(schema, { space: 2 })` so the * persisted JSON is the schema's canonical encoding rather than a raw * `JSON.stringify`, and any I/O failure is surfaced as a typed * {@link JsonFileWriteError}. @@ -243,7 +246,7 @@ const writeJsonFile = ({ }) }) -const sidecarJson = Schema.parseJson(FilesystemWorkspaceSidecar, { space: 2 }) +const sidecarJson = Schema.fromJsonString(FilesystemWorkspaceSidecar, { space: 2 }) const writeDatasourceSyncBodySidecar = ({ root, @@ -409,7 +412,7 @@ export const makeNotionMdPageBodySyncPort = ({ expectedLocalBodyHash: command.nextBodyHash, }).pipe( provideNotionMdGatewayAndStateStore({ gateway, stateStore }), - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), ) yield* writeDatasourceSyncBodySidecar({ root, @@ -552,7 +555,7 @@ export const makeNotionMdMaterializingLocalWorkspacePort = ({ ...(plan.writableProperties === undefined ? {} : { properties: plan.writableProperties }), }).pipe( provideNotionMdGatewayAndStateStore({ gateway, stateStore }), - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), Effect.mapError( (cause) => new LocalStoreError({ diff --git a/packages/@overeng/notion-datasource-sync/src/cli/effect-command.ts b/packages/@overeng/notion-datasource-sync/src/cli/effect-command.ts index da9993417d..28d0e05c36 100644 --- a/packages/@overeng/notion-datasource-sync/src/cli/effect-command.ts +++ b/packages/@overeng/notion-datasource-sync/src/cli/effect-command.ts @@ -1,5 +1,5 @@ -import { Args, Command, Options } from '@effect/cli' import { Effect } from 'effect' +import { Argument as Args, Command, Flag as Options } from 'effect/unstable/cli' /** Handler used by the import-safe command descriptor for executable leaf commands. */ export type DatasourceDbCommandHandler = (command: string) => Effect.Effect @@ -9,7 +9,7 @@ export type CompletionShell = 'bash' | 'fish' | 'sh' | 'zsh' const defaultHandler: DatasourceDbCommandHandler = () => Effect.void -const workspaceRootArg = Args.text({ name: 'workspace-root' }).pipe( +const workspaceRootArg = Args.string('workspace-root').pipe( Args.withDescription('Workspace root or SQLite replica path'), Args.optional, ) @@ -29,12 +29,12 @@ const sqliteOption = Options.file('sqlite').pipe( Options.optional, ) -const rootIdOption = Options.text('root-id').pipe( +const rootIdOption = Options.string('root-id').pipe( Options.withDescription('Sync root id'), Options.optional, ) -const dataSourceIdOption = Options.text('data-source-id').pipe( +const dataSourceIdOption = Options.string('data-source-id').pipe( Options.withDescription('Notion data source id'), Options.optional, ) @@ -132,7 +132,7 @@ export const makeDatasourceDbSubcommands = ( description: 'Resolve a conflict', handler, extraConfig: { - conflictId: Options.text('conflict-id').pipe( + conflictId: Options.string('conflict-id').pipe( Options.withDescription('Conflict id to resolve'), Options.optional, ), @@ -140,7 +140,7 @@ export const makeDatasourceDbSubcommands = ( Options.withDescription('Conflict resolution strategy'), Options.optional, ), - valueJson: Options.text('value-json').pipe( + valueJson: Options.string('value-json').pipe( Options.withDescription('Manual resolution value as JSON'), Options.optional, ), @@ -155,7 +155,7 @@ export const makeDatasourceDbSubcommands = ( 'track', { ...commonOptions, - remoteRef: Args.text({ name: 'remote-ref' }).pipe( + remoteRef: Args.string('remote-ref').pipe( Args.withDescription('Notion data source or database URL to adopt'), Args.optional, ), @@ -216,7 +216,7 @@ export const makeDatasourceDbSubcommands = ( description: 'Archive or forget a page locally', handler, extraConfig: { - pageId: Options.text('page-id').pipe( + pageId: Options.string('page-id').pipe( Options.withDescription('Notion page id'), Options.optional, ), @@ -228,7 +228,7 @@ export const makeDatasourceDbSubcommands = ( description: 'Restore a forgotten page locally', handler, extraConfig: { - pageId: Options.text('page-id').pipe( + pageId: Options.string('page-id').pipe( Options.withDescription('Notion page id'), Options.optional, ), diff --git a/packages/@overeng/notion-datasource-sync/src/cli/main.ts b/packages/@overeng/notion-datasource-sync/src/cli/main.ts index e23076c55a..63afcbb175 100755 --- a/packages/@overeng/notion-datasource-sync/src/cli/main.ts +++ b/packages/@overeng/notion-datasource-sync/src/cli/main.ts @@ -6,9 +6,9 @@ import { existsSync, mkdirSync } from 'node:fs' import { dirname, isAbsolute, join, resolve } from 'node:path' import { DatabaseSync } from 'node:sqlite' -import { FetchHttpClient } from '@effect/platform' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect, Either, Layer, Option, Redacted, Schema, Stream } from 'effect' +import { FetchHttpClient } from 'effect/unstable/http' import { NOTION_API_VERSION, @@ -782,9 +782,12 @@ export type CliErrorEnvelope = { } /** Thrown during argument parsing when a required flag is missing, invalid, or unsupported. */ -export class CliArgumentError extends Schema.TaggedError()('CliArgumentError', { - message: Schema.String, -}) {} +export class CliArgumentError extends Schema.TaggedErrorClass()( + 'CliArgumentError', + { + message: Schema.String, + }, +) {} /** * Narrow a thrown value from the synchronous CLI parsers into the typed @@ -826,12 +829,12 @@ export const serviceNameForCliCommand = (command: CliCommand): string => const SchemaPropertyObservationJson = Schema.Struct({ propertyId: PropertyId, - name: Schema.optional(Schema.NonEmptyTrimmedString), - type: Schema.optional(Schema.NonEmptyTrimmedString), + name: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), + type: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), configHash: Hash, - writeClass: Schema.Literal('writable', 'computed', 'unsupported'), + writeClass: Schema.Literals(['writable', 'computed', 'unsupported']), configJson: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.Cli.SchemaPropertyObservationJson' }) +}).annotate({ identifier: 'NotionDatasourceSync.Cli.SchemaPropertyObservationJson' }) const capabilityNames = new Set(allGatewayCapabilities) @@ -851,7 +854,7 @@ const decodeJson = ({ readonly value: string }): typeof schema.Type => Schema.decodeUnknownSync(schema)( - Schema.decodeUnknownSync(Schema.parseJson(Schema.Unknown))(value), + Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(value), ) const withOptionalRuntimeOptions = (context: CliContext) => ({ @@ -2990,7 +2993,7 @@ export const makeCliRuntimeLayer = ({ ? undefined : Layer.mergeAll( NotionMdGatewayLive.pipe(Layer.provide(liveBaseLayer)), - NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)), + NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)), ) const gatewayLayer = options.gateway !== undefined diff --git a/packages/@overeng/notion-datasource-sync/src/cli/progress.ts b/packages/@overeng/notion-datasource-sync/src/cli/progress.ts index 3e6347b1ad..129cb7dc33 100644 --- a/packages/@overeng/notion-datasource-sync/src/cli/progress.ts +++ b/packages/@overeng/notion-datasource-sync/src/cli/progress.ts @@ -5,7 +5,7 @@ import { Box, Text, createTuiApp, useTuiAtomValue, type TuiApp } from '@overeng/ import type { SyncProgressEvent, SyncProgressPhase } from '../core/progress.ts' -const Phase = Schema.Literal( +const Phase = Schema.Literals([ 'preparing', 'pulling', 'querying', @@ -16,7 +16,7 @@ const Phase = Schema.Literal( 'projecting', 'watching', 'complete', -) +]) const SyncProgressState = Schema.Struct({ command: Schema.String, @@ -37,14 +37,14 @@ const SyncProgressState = Schema.Struct({ export type SyncProgressState = typeof SyncProgressState.Type -const SyncProgressAction = Schema.Union( +const SyncProgressAction = Schema.Union([ Schema.TaggedStruct('SetState', { state: SyncProgressState, }), Schema.TaggedStruct('ApplyEvent', { event: Schema.Any, }), -) +]) export type SyncProgressAction = typeof SyncProgressAction.Type diff --git a/packages/@overeng/notion-datasource-sync/src/core/commands.ts b/packages/@overeng/notion-datasource-sync/src/core/commands.ts index 1bcc7f5d9e..86c0a1d1ab 100644 --- a/packages/@overeng/notion-datasource-sync/src/core/commands.ts +++ b/packages/@overeng/notion-datasource-sync/src/core/commands.ts @@ -1,4 +1,4 @@ -import { Schema } from 'effect' +import { Effect, Schema } from 'effect' import { CanonicalFileValue as SchemaCanonicalFileValue, @@ -24,17 +24,17 @@ import { } from './domain.ts' /** Defines how row membership is determined: `all-data-source-rows` treats any absence as potential removal; `explicit-filter` limits absence proofs to the filter scope. */ -export const QueryMembershipScope = Schema.Literal( +export const QueryMembershipScope = Schema.Literals([ 'all-data-source-rows', 'explicit-filter', -).annotations({ identifier: 'NotionDatasourceSync.QueryMembershipScope' }) +]).annotate({ identifier: 'NotionDatasourceSync.QueryMembershipScope' }) export type QueryMembershipScope = typeof QueryMembershipScope.Type /** Canonical sort specification for a Notion query: property-based with an explicit direction. */ export const CanonicalNotionSort = Schema.TaggedStruct('CanonicalNotionSort', { propertyId: PropertyId, - direction: Schema.Literal('ascending', 'descending'), -}).annotations({ identifier: 'NotionDatasourceSync.CanonicalNotionSort' }) + direction: Schema.Literals(['ascending', 'descending']), +}).annotate({ identifier: 'NotionDatasourceSync.CanonicalNotionSort' }) export type CanonicalNotionSort = typeof CanonicalNotionSort.Type /** @@ -63,17 +63,17 @@ export const CanonicalPropertyValue = SchemaCanonicalPropertyValue export type CanonicalPropertyValue = typeof CanonicalPropertyValue.Type /** Stable data-source icon identity; transient Notion-hosted signed URLs are intentionally excluded from the canonical surface. */ -export const CanonicalDataSourceIcon = Schema.Union( +export const CanonicalDataSourceIcon = Schema.Union([ Schema.TaggedStruct('none', {}), Schema.TaggedStruct('emoji', { emoji: Schema.String }), - Schema.TaggedStruct('custom_emoji', { id: Schema.NonEmptyTrimmedString }), + Schema.TaggedStruct('custom_emoji', { id: Schema.Trimmed.check(Schema.isNonEmpty()) }), Schema.TaggedStruct('notion_icon', { - name: Schema.NonEmptyTrimmedString, - color: Schema.optional(Schema.NonEmptyTrimmedString), + name: Schema.Trimmed.check(Schema.isNonEmpty()), + color: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), }), Schema.TaggedStruct('external', { urlHash: Hash }), Schema.TaggedStruct('transient_file', {}), -).annotations({ identifier: 'NotionDatasourceSync.CanonicalDataSourceIcon' }) +]).annotate({ identifier: 'NotionDatasourceSync.CanonicalDataSourceIcon' }) export type CanonicalDataSourceIcon = typeof CanonicalDataSourceIcon.Type /** Canonical data-source metadata kept separate from schema, rows, and body materialization. */ @@ -81,14 +81,14 @@ export const CanonicalDataSourceMetadata = Schema.TaggedStruct('CanonicalDataSou titlePlainText: Schema.String, descriptionPlainText: Schema.String, icon: CanonicalDataSourceIcon, -}).annotations({ identifier: 'NotionDatasourceSync.CanonicalDataSourceMetadata' }) +}).annotate({ identifier: 'NotionDatasourceSync.CanonicalDataSourceMetadata' }) export type CanonicalDataSourceMetadata = typeof CanonicalDataSourceMetadata.Type /** Canonical schema descriptor for a single Notion database property; `configHash` covers all type-configuration details not in the `type` discriminator. */ export const CanonicalDataSourceProperty = Schema.TaggedStruct('CanonicalDataSourceProperty', { propertyId: PropertyId, name: PropertyName, - type: Schema.Literal( + type: Schema.Literals([ 'title', 'rich_text', 'number', @@ -109,17 +109,17 @@ export const CanonicalDataSourceProperty = Schema.TaggedStruct('CanonicalDataSou 'created_by', 'last_edited_time', 'last_edited_by', - ), + ]), configHash: Hash, -}).annotations({ identifier: 'NotionDatasourceSync.CanonicalDataSourceProperty' }) +}).annotate({ identifier: 'NotionDatasourceSync.CanonicalDataSourceProperty' }) export type CanonicalDataSourceProperty = typeof CanonicalDataSourceProperty.Type /** Canonical filter expression for a Notion query; complex filters are represented as a hash to keep the contract stable. */ -export const CanonicalNotionFilter = Schema.Union( +export const CanonicalNotionFilter = Schema.Union([ Schema.TaggedStruct('none', {}), Schema.TaggedStruct('property_value', { propertyId: PropertyId, - operator: Schema.Literal( + operator: Schema.Literals([ 'equals', 'does_not_equal', 'contains', @@ -132,14 +132,14 @@ export const CanonicalNotionFilter = Schema.Union( 'less_than', 'on_or_before', 'on_or_after', - ), + ]), value: Schema.NullOr(CanonicalPropertyValue), }), Schema.TaggedStruct('compound_hash', { - kind: Schema.Literal('and', 'or'), + kind: Schema.Literals(['and', 'or']), expressionHash: Hash, }), -).annotations({ identifier: 'NotionDatasourceSync.CanonicalNotionFilter' }) +]).annotate({ identifier: 'NotionDatasourceSync.CanonicalNotionFilter' }) export type CanonicalNotionFilter = typeof CanonicalNotionFilter.Type /** Immutable contract describing how rows are queried; any change invalidates prior absence proofs and query checkpoints. */ @@ -150,7 +150,7 @@ export const QueryContract = Schema.TaggedStruct('QueryContract', { pageSize: NotionPageSize, highWatermark: Schema.NullOr(Schema.DateTimeUtc), membershipScope: QueryMembershipScope, -}).annotations({ identifier: 'NotionDatasourceSync.QueryContract' }) +}).annotate({ identifier: 'NotionDatasourceSync.QueryContract' }) export type QueryContract = typeof QueryContract.Type /** Input to a paginated row query: identifies the data source, the query contract, and the optional resume cursor. */ @@ -158,7 +158,7 @@ export const QueryRowsInput = Schema.TaggedStruct('QueryRowsInput', { dataSourceId: DataSourceId, queryContract: QueryContract, startCursor: Schema.NullOr(QueryCursor), -}).annotations({ identifier: 'NotionDatasourceSync.QueryRowsInput' }) +}).annotate({ identifier: 'NotionDatasourceSync.QueryRowsInput' }) export type QueryRowsInput = typeof QueryRowsInput.Type /** A single page of query results; `hasMore` and `nextCursor` drive pagination, `cappedAtLimit` indicates a result-cap was hit. */ @@ -171,7 +171,7 @@ export const QueryRowsPage = Schema.TaggedStruct('QueryRowsPage', { pageId: PageId, dataSourceId: Schema.optional(DataSourceId), propertiesHash: Hash, - propertyValuesJson: Schema.optional(Schema.Record({ key: PropertyId, value: Schema.String })), + propertyValuesJson: Schema.optional(Schema.Record(PropertyId, Schema.String)), lastEditedTime: Schema.DateTimeUtc, inTrash: Schema.Boolean, }), @@ -179,7 +179,7 @@ export const QueryRowsPage = Schema.TaggedStruct('QueryRowsPage', { nextCursor: Schema.NullOr(QueryCursor), hasMore: Schema.Boolean, cappedAtLimit: Schema.Boolean, -}).annotations({ identifier: 'NotionDatasourceSync.QueryRowsPage' }) +}).annotate({ identifier: 'NotionDatasourceSync.QueryRowsPage' }) export type QueryRowsPage = typeof QueryRowsPage.Type /** Input to a paginated property-value retrieval for a specific page + property combination. */ @@ -187,7 +187,7 @@ export const RetrievePagePropertyInput = Schema.TaggedStruct('RetrievePageProper pageId: PageId, propertyId: PropertyId, startCursor: Schema.NullOr(QueryCursor), -}).annotations({ identifier: 'NotionDatasourceSync.RetrievePagePropertyInput' }) +}).annotate({ identifier: 'NotionDatasourceSync.RetrievePagePropertyInput' }) export type RetrievePagePropertyInput = typeof RetrievePagePropertyInput.Type /** A single page of property items returned by a paginated property retrieval. */ @@ -200,7 +200,7 @@ export const PagePropertyItemPage = Schema.TaggedStruct('PagePropertyItemPage', listMetadataHash: Schema.optional(Hash), nextCursor: Schema.NullOr(QueryCursor), hasMore: Schema.Boolean, -}).annotations({ identifier: 'NotionDatasourceSync.PagePropertyItemPage' }) +}).annotate({ identifier: 'NotionDatasourceSync.PagePropertyItemPage' }) export type PagePropertyItemPage = typeof PagePropertyItemPage.Type /** Remote write command: applies a partial property patch to a Notion page, gated on the base properties hash matching. */ @@ -208,18 +208,18 @@ export const PatchPagePropertiesCommand = Schema.TaggedStruct('PatchPageProperti commandId: CommandId, pageId: PageId, basePropertiesHash: Hash, - propertyPatch: Schema.Record({ key: PropertyId, value: CanonicalPropertyValue }), -}).annotations({ identifier: 'NotionDatasourceSync.PatchPagePropertiesCommand' }) + propertyPatch: Schema.Record(PropertyId, CanonicalPropertyValue), +}).annotate({ identifier: 'NotionDatasourceSync.PatchPagePropertiesCommand' }) export type PatchPagePropertiesCommand = typeof PatchPagePropertiesCommand.Type /** Remote write command: creates a new Notion page in a data source with initial property values. */ export const CreatePageCommand = Schema.TaggedStruct('CreatePageCommand', { commandId: CommandId, dataSourceId: DataSourceId, - clientRequestKey: Schema.NonEmptyTrimmedString, + clientRequestKey: Schema.Trimmed.check(Schema.isNonEmpty()), baseSchemaHash: Hash, - initialProperties: Schema.Record({ key: PropertyId, value: CanonicalPropertyValue }), -}).annotations({ identifier: 'NotionDatasourceSync.CreatePageCommand' }) + initialProperties: Schema.Record(PropertyId, CanonicalPropertyValue), +}).annotate({ identifier: 'NotionDatasourceSync.CreatePageCommand' }) export type CreatePageCommand = typeof CreatePageCommand.Type /** Result of a successful Notion page creation, carrying the server-minted page id. */ @@ -227,7 +227,7 @@ export const CreatePageResult = Schema.TaggedStruct('CreatePageResult', { requestId: NotionRequestId, pageId: PageId, propertiesHash: Hash, -}).annotations({ identifier: 'NotionDatasourceSync.CreatePageResult' }) +}).annotate({ identifier: 'NotionDatasourceSync.CreatePageResult' }) export type CreatePageResult = typeof CreatePageResult.Type /** @@ -241,7 +241,7 @@ export type CreatePageResult = typeof CreatePageResult.Type * the properties that cannot be updated via the API; advertising it here * would suggest behavior the adapter cannot actually deliver. */ -export const AddPropertyDefinition = Schema.Union( +export const AddPropertyDefinition = Schema.Union([ Schema.TaggedStruct('rich_text', {}), Schema.TaggedStruct('number', {}), Schema.TaggedStruct('checkbox', {}), @@ -252,7 +252,7 @@ export const AddPropertyDefinition = Schema.Union( Schema.TaggedStruct('people', {}), Schema.TaggedStruct('select', { options: Schema.Array(CanonicalOptionValue) }), Schema.TaggedStruct('multi_select', { options: Schema.Array(CanonicalOptionValue) }), -).annotations({ identifier: 'NotionDatasourceSync.AddPropertyDefinition' }) +]).annotate({ identifier: 'NotionDatasourceSync.AddPropertyDefinition' }) export type AddPropertyDefinition = typeof AddPropertyDefinition.Type /** @@ -272,7 +272,7 @@ export type AddPropertyDefinition = typeof AddPropertyDefinition.Type * options) are deliberately absent so unsupported intents fail closed at the * planner/adapter boundary. */ -export const SchemaPatchOperation = Schema.Union( +export const SchemaPatchOperation = Schema.Union([ Schema.TaggedStruct('AddProperty', { name: PropertyName, definition: AddPropertyDefinition, @@ -283,11 +283,11 @@ export const SchemaPatchOperation = Schema.Union( }), Schema.TaggedStruct('AddSelectOptions', { propertyId: PropertyId, - propertyType: Schema.Literal('select', 'multi_select'), + propertyType: Schema.Literals(['select', 'multi_select']), existingOptions: Schema.Array(CanonicalOptionValue), newOptions: Schema.Array(CanonicalOptionValue), }), -).annotations({ identifier: 'NotionDatasourceSync.SchemaPatchOperation' }) +]).annotate({ identifier: 'NotionDatasourceSync.SchemaPatchOperation' }) export type SchemaPatchOperation = typeof SchemaPatchOperation.Type /** @@ -304,11 +304,12 @@ export const PatchDataSourceSchemaCommand = Schema.TaggedStruct('PatchDataSource commandId: CommandId, dataSourceId: DataSourceId, baseSchemaHash: Hash, - schemaPatch: Schema.Record({ key: PropertyId, value: CanonicalDataSourceProperty }), - operations: Schema.optionalWith(Schema.Array(SchemaPatchOperation), { - default: () => [] as ReadonlyArray, - }), -}).annotations({ identifier: 'NotionDatasourceSync.PatchDataSourceSchemaCommand' }) + schemaPatch: Schema.Record(PropertyId, CanonicalDataSourceProperty), + operations: Schema.Array(SchemaPatchOperation).pipe( + Schema.withDecodingDefaultType(Effect.sync(() => [] as ReadonlyArray)), + Schema.withConstructorDefault(Effect.sync(() => [] as ReadonlyArray)), + ), +}).annotate({ identifier: 'NotionDatasourceSync.PatchDataSourceSchemaCommand' }) export type PatchDataSourceSchemaCommand = typeof PatchDataSourceSchemaCommand.Type /** Remote write command: patches data-source presentation metadata, gated on the base metadata hash matching. */ @@ -323,7 +324,7 @@ export const PatchDataSourceMetadataCommand = Schema.TaggedStruct( descriptionPlainText: Schema.optional(Schema.String), }), }, -).annotations({ identifier: 'NotionDatasourceSync.PatchDataSourceMetadataCommand' }) +).annotate({ identifier: 'NotionDatasourceSync.PatchDataSourceMetadataCommand' }) export type PatchDataSourceMetadataCommand = typeof PatchDataSourceMetadataCommand.Type /** Remote write command: patches database/container metadata, verified through the owning data source metadata projection. */ @@ -336,7 +337,7 @@ export const PatchDatabaseMetadataCommand = Schema.TaggedStruct('PatchDatabaseMe titlePlainText: Schema.optional(Schema.String), descriptionPlainText: Schema.optional(Schema.String), }), -}).annotations({ identifier: 'NotionDatasourceSync.PatchDatabaseMetadataCommand' }) +}).annotate({ identifier: 'NotionDatasourceSync.PatchDatabaseMetadataCommand' }) export type PatchDatabaseMetadataCommand = typeof PatchDatabaseMetadataCommand.Type /** Remote write command: moves a Notion page to the trash, gated on the base properties hash. */ @@ -344,7 +345,7 @@ export const TrashPageCommand = Schema.TaggedStruct('TrashPageCommand', { commandId: CommandId, pageId: PageId, basePropertiesHash: Hash, -}).annotations({ identifier: 'NotionDatasourceSync.TrashPageCommand' }) +}).annotate({ identifier: 'NotionDatasourceSync.TrashPageCommand' }) export type TrashPageCommand = typeof TrashPageCommand.Type /** Remote write command: restores a trashed Notion page, gated on the base properties hash. */ @@ -352,13 +353,13 @@ export const RestorePageCommand = Schema.TaggedStruct('RestorePageCommand', { commandId: CommandId, pageId: PageId, basePropertiesHash: Hash, -}).annotations({ identifier: 'NotionDatasourceSync.RestorePageCommand' }) +}).annotate({ identifier: 'NotionDatasourceSync.RestorePageCommand' }) export type RestorePageCommand = typeof RestorePageCommand.Type /** Input to `PageBodySyncPort.observe`: fetches the current body state and returns a `BodyPointer`. */ export const ObserveBodyInput = Schema.TaggedStruct('ObserveBodyInput', { pageId: PageId, -}).annotations({ identifier: 'NotionDatasourceSync.ObserveBodyInput' }) +}).annotate({ identifier: 'NotionDatasourceSync.ObserveBodyInput' }) export type ObserveBodyInput = typeof ObserveBodyInput.Type /** Input to `PageBodySyncPort.planLocalChange`: describes the local body modification to be evaluated for conflicts or intent promotion. */ @@ -368,7 +369,7 @@ export const BodyLocalChangeInput = Schema.TaggedStruct('BodyLocalChangeInput', localBodyHash: Hash, localBodyPath: Schema.optional(WorkspaceRelativePath), localBodyContent: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.BodyLocalChangeInput' }) +}).annotate({ identifier: 'NotionDatasourceSync.BodyLocalChangeInput' }) export type BodyLocalChangeInput = typeof BodyLocalChangeInput.Type /** A confirmed local body intent: the desired next body hash relative to a known base pointer, ready to become a `BodyPushCommand`. */ @@ -378,11 +379,11 @@ export const BodyIntent = Schema.TaggedStruct('BodyIntent', { nextBodyHash: Hash, localBodyPath: Schema.optional(WorkspaceRelativePath), localBodyContent: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.BodyIntent' }) +}).annotate({ identifier: 'NotionDatasourceSync.BodyIntent' }) export type BodyIntent = typeof BodyIntent.Type /** Named reason for a body conflict; maps directly to the `GuardName` values that can block a body push. */ -export const BodyConflictReason = Schema.Literal( +export const BodyConflictReason = Schema.Literals([ 'StaleSurfaceBase', 'BodyLossyRemote', 'MarkdownUnknownBlocksAmbiguous', @@ -391,7 +392,7 @@ export const BodyConflictReason = Schema.Literal( 'MarkdownSyncedPageUnsupported', 'BodyAdapterConflict', 'BodyAdapterNonBodyMutation', -).annotations({ identifier: 'NotionDatasourceSync.BodyConflictReason' }) +]).annotate({ identifier: 'NotionDatasourceSync.BodyConflictReason' }) export type BodyConflictReason = typeof BodyConflictReason.Type /** Conflict detected by the body adapter between local and remote body states; prevents the push from proceeding without resolution. */ @@ -402,7 +403,7 @@ export const BodyConflict = Schema.TaggedStruct('BodyConflict', { remoteBodyHash: Hash, reason: Schema.optional(BodyConflictReason), message: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.BodyConflict' }) +}).annotate({ identifier: 'NotionDatasourceSync.BodyConflict' }) export type BodyConflict = typeof BodyConflict.Type /** Remote write command: pushes the next body hash to Notion, gated on the base body pointer. */ @@ -413,7 +414,7 @@ export const BodyPushCommand = Schema.TaggedStruct('BodyPushCommand', { nextBodyHash: Hash, localBodyPath: Schema.optional(WorkspaceRelativePath), localBodyContent: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.BodyPushCommand' }) +}).annotate({ identifier: 'NotionDatasourceSync.BodyPushCommand' }) export type BodyPushCommand = typeof BodyPushCommand.Type /** Result of a successful body push; carries the updated `BodyPointer` for post-write verification. */ @@ -421,18 +422,18 @@ export const BodyPushResult = Schema.TaggedStruct('BodyPushResult', { pageId: PageId, requestId: NotionRequestId, bodyPointer: BodyPointer, -}).annotations({ identifier: 'NotionDatasourceSync.BodyPushResult' }) +}).annotate({ identifier: 'NotionDatasourceSync.BodyPushResult' }) export type BodyPushResult = typeof BodyPushResult.Type /** Input to `PageBodySyncPort.repair`: re-reads the current body state to reconcile a prior desynchronization. */ export const BodyRepairInput = Schema.TaggedStruct('BodyRepairInput', { pageId: PageId, currentBodyPointer: BodyPointer, -}).annotations({ identifier: 'NotionDatasourceSync.BodyRepairInput' }) +}).annotate({ identifier: 'NotionDatasourceSync.BodyRepairInput' }) export type BodyRepairInput = typeof BodyRepairInput.Type /** Discriminated union of all commands that cause a remote write to Notion; the `_tag` selects the operation kind. */ -export const RemoteWriteCommand = Schema.Union( +export const RemoteWriteCommand = Schema.Union([ CreatePageCommand, PatchPagePropertiesCommand, PatchDataSourceSchemaCommand, @@ -441,11 +442,11 @@ export const RemoteWriteCommand = Schema.Union( TrashPageCommand, RestorePageCommand, BodyPushCommand, -).annotations({ identifier: 'NotionDatasourceSync.RemoteWriteCommand' }) +]).annotate({ identifier: 'NotionDatasourceSync.RemoteWriteCommand' }) export type RemoteWriteCommand = typeof RemoteWriteCommand.Type /** Payload stored in the outbox for a planned remote write; wraps the command so the outbox projection can decode it without knowing the concrete type. */ export const RemoteWritePlanPayload = Schema.Struct({ command: RemoteWriteCommand, -}).annotations({ identifier: 'NotionDatasourceSync.RemoteWritePlanPayload' }) +}).annotate({ identifier: 'NotionDatasourceSync.RemoteWritePlanPayload' }) export type RemoteWritePlanPayload = typeof RemoteWritePlanPayload.Type diff --git a/packages/@overeng/notion-datasource-sync/src/core/conflicts.ts b/packages/@overeng/notion-datasource-sync/src/core/conflicts.ts index 3031570d2a..fb9d2dbe04 100644 --- a/packages/@overeng/notion-datasource-sync/src/core/conflicts.ts +++ b/packages/@overeng/notion-datasource-sync/src/core/conflicts.ts @@ -4,7 +4,7 @@ import type { Hash, PageId, PropertyId } from './domain.ts' import type { SurfaceKey } from './events.ts' /** Discriminator for the type of conflict detected between local and remote changes. */ -export const ConflictKind = Schema.Literal( +export const ConflictKind = Schema.Literals([ 'same-property', 'disjoint-property', 'property-vs-body', @@ -16,7 +16,7 @@ export const ConflictKind = Schema.Literal( 'path-collision', 'lossy-body', 'permission-ambiguous', -).annotations({ identifier: 'NotionDatasourceSync.ConflictKind' }) +]).annotate({ identifier: 'NotionDatasourceSync.ConflictKind' }) export type ConflictKind = typeof ConflictKind.Type /** A change surface representing a property-value mutation on a specific page; carries base and next hashes for three-way merge. */ diff --git a/packages/@overeng/notion-datasource-sync/src/core/domain.ts b/packages/@overeng/notion-datasource-sync/src/core/domain.ts index 9607e2f559..b93e8a95c4 100644 --- a/packages/@overeng/notion-datasource-sync/src/core/domain.ts +++ b/packages/@overeng/notion-datasource-sync/src/core/domain.ts @@ -15,15 +15,15 @@ import { } from '@overeng/notion-effect-schema' /** The single Notion API version this package is tested and certified against. */ -export const SupportedNotionApiVersion = Schema.Literal(NOTION_API_VERSION).annotations({ +export const SupportedNotionApiVersion = Schema.Literal(NOTION_API_VERSION).annotate({ identifier: 'NotionDatasourceSync.SupportedNotionApiVersion', }) export type SupportedNotionApiVersion = typeof SupportedNotionApiVersion.Type /** Branded version string identifying the datasource-sync client build. */ -export const ClientVersion = Schema.NonEmptyTrimmedString.pipe( +export const ClientVersion = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.ClientVersion'), - Schema.annotations({ identifier: 'NotionDatasourceSync.ClientVersion' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.ClientVersion' }), ) export type ClientVersion = typeof ClientVersion.Type @@ -35,9 +35,9 @@ export const DataSourceId = SchemaDataSourceId export type DataSourceId = typeof DataSourceId.Type /** Branded Notion database/container ID; distinct from a v2 data-source ID. */ -export const DatabaseId = Schema.NonEmptyTrimmedString.pipe( +export const DatabaseId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.DatabaseId'), - Schema.annotations({ identifier: 'NotionDatasourceSync.DatabaseId' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.DatabaseId' }), ) export type DatabaseId = typeof DatabaseId.Type @@ -59,9 +59,9 @@ export const PropertyId = SchemaPropertyId export type PropertyId = typeof PropertyId.Type /** Branded Notion view ID, distinct from local generated SQLite projection views. */ -export const ViewId = Schema.NonEmptyTrimmedString.pipe( +export const ViewId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.ViewId'), - Schema.annotations({ identifier: 'NotionDatasourceSync.ViewId' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.ViewId' }), ) export type ViewId = typeof ViewId.Type @@ -73,57 +73,57 @@ export const PropertyName = SchemaPropertyName export type PropertyName = typeof PropertyName.Type /** Branded ID that uniquely identifies a remote-write command in the outbox. */ -export const CommandId = Schema.NonEmptyTrimmedString.pipe( +export const CommandId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.CommandId'), - Schema.annotations({ identifier: 'NotionDatasourceSync.CommandId' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.CommandId' }), ) export type CommandId = typeof CommandId.Type /** Branded opaque cursor returned by paginated Notion query endpoints. */ -export const QueryCursor = Schema.NonEmptyTrimmedString.pipe( +export const QueryCursor = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.QueryCursor'), - Schema.annotations({ identifier: 'NotionDatasourceSync.QueryCursor' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.QueryCursor' }), ) export type QueryCursor = typeof QueryCursor.Type /** Branded request ID returned by the Notion API; used for idempotency tracking and audit trails. */ -export const NotionRequestId = Schema.NonEmptyTrimmedString.pipe( +export const NotionRequestId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.NotionRequestId'), - Schema.annotations({ identifier: 'NotionDatasourceSync.NotionRequestId' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.NotionRequestId' }), ) export type NotionRequestId = typeof NotionRequestId.Type /** SHA-256 content hash used as a stable identity for Notion objects and local artifacts (format: `sha256:`). */ export const Hash = ContentDigest.pipe( Schema.brand('NotionDatasourceSync.Hash'), - Schema.annotations({ identifier: 'NotionDatasourceSync.Hash' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.Hash' }), ) export type Hash = typeof Hash.Type /** Body-scoped evidence fingerprint derived from a full remote body observation envelope. */ export const BodyEvidenceFingerprint = ContentDigest.pipe( Schema.brand('NotionDatasourceSync.BodyEvidenceFingerprint'), - Schema.annotations({ identifier: 'NotionDatasourceSync.BodyEvidenceFingerprint' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.BodyEvidenceFingerprint' }), ) export type BodyEvidenceFingerprint = typeof BodyEvidenceFingerprint.Type /** Completeness classification carried by a remote body evidence-backed identity. */ -export const BodyCompletenessEvidence = Schema.Literal('complete', 'lossy').annotations({ +export const BodyCompletenessEvidence = Schema.Literals(['complete', 'lossy']).annotate({ identifier: 'NotionDatasourceSync.BodyCompletenessEvidence', }) export type BodyCompletenessEvidence = typeof BodyCompletenessEvidence.Type /** Branded absolute filesystem path to the workspace root or a local artifact. */ -export const AbsolutePath = Schema.NonEmptyTrimmedString.pipe( +export const AbsolutePath = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.AbsolutePath'), - Schema.annotations({ identifier: 'NotionDatasourceSync.AbsolutePath' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.AbsolutePath' }), ) export type AbsolutePath = typeof AbsolutePath.Type /** Branded path relative to the sync workspace root, used in path-claim and materialization operations. */ -export const WorkspaceRelativePath = Schema.NonEmptyTrimmedString.pipe( +export const WorkspaceRelativePath = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.WorkspaceRelativePath'), - Schema.annotations({ identifier: 'NotionDatasourceSync.WorkspaceRelativePath' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.WorkspaceRelativePath' }), ) export type WorkspaceRelativePath = typeof WorkspaceRelativePath.Type @@ -131,9 +131,9 @@ export type WorkspaceRelativePath = typeof WorkspaceRelativePath.Type * Token written alongside a materialized file that suppresses a spurious local-change event * when the sync engine itself is the author of the filesystem write. */ -export const OwnWriteSuppressionToken = Schema.NonEmptyTrimmedString.pipe( +export const OwnWriteSuppressionToken = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.OwnWriteSuppressionToken'), - Schema.annotations({ identifier: 'NotionDatasourceSync.OwnWriteSuppressionToken' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.OwnWriteSuppressionToken' }), ) export type OwnWriteSuppressionToken = typeof OwnWriteSuppressionToken.Type @@ -141,7 +141,7 @@ export type OwnWriteSuppressionToken = typeof OwnWriteSuppressionToken.Type export const PositiveInt = Schema.Number.pipe( Schema.int(), Schema.positive(), - Schema.annotations({ identifier: 'NotionDatasourceSync.PositiveInt' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.PositiveInt' }), ) export type PositiveInt = typeof PositiveInt.Type @@ -149,12 +149,12 @@ export type PositiveInt = typeof PositiveInt.Type export const NotionPageSize = Schema.Number.pipe( Schema.int(), Schema.between(1, 100), - Schema.annotations({ identifier: 'NotionDatasourceSync.NotionPageSize' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.NotionPageSize' }), ) export type NotionPageSize = typeof NotionPageSize.Type /** Named Notion API capability checked during preflight; governs which operations the sync engine may attempt. */ -export const CapabilityName = Schema.Literal( +export const CapabilityName = Schema.Literals([ 'data_source_retrieve', 'data_source_query', 'data_source_metadata_update', @@ -166,7 +166,7 @@ export const CapabilityName = Schema.Literal( 'schema_update', 'page_trash', 'page_restore', -).annotations({ identifier: 'NotionDatasourceSync.CapabilityName' }) +]).annotate({ identifier: 'NotionDatasourceSync.CapabilityName' }) export type CapabilityName = typeof CapabilityName.Type /** Observed Notion API contract: the version reported by the gateway and the set of capabilities it declared as supported. */ @@ -174,14 +174,14 @@ export const NotionApiContract = Schema.TaggedStruct('NotionApiContract', { apiVersion: SupportedNotionApiVersion, clientVersion: ClientVersion, supportedCapabilities: Schema.Array(CapabilityName), -}).annotations({ identifier: 'NotionDatasourceSync.NotionApiContract' }) +}).annotate({ identifier: 'NotionDatasourceSync.NotionApiContract' }) export type NotionApiContract = typeof NotionApiContract.Type /** Input to a capability preflight check: identifies the data source and the capabilities required for the planned operation. */ export const CapabilityPreflightInput = Schema.TaggedStruct('CapabilityPreflightInput', { dataSourceId: DataSourceId, requiredCapabilities: Schema.Array(CapabilityName), -}).annotations({ identifier: 'NotionDatasourceSync.CapabilityPreflightInput' }) +}).annotate({ identifier: 'NotionDatasourceSync.CapabilityPreflightInput' }) export type CapabilityPreflightInput = typeof CapabilityPreflightInput.Type /** Result of a capability preflight check; exposes which capabilities were present and which were missing. */ @@ -190,27 +190,27 @@ export const CapabilityPreflightResult = Schema.TaggedStruct('CapabilityPrefligh apiContract: NotionApiContract, supportedCapabilities: Schema.Array(CapabilityName), missingCapabilities: Schema.Array(CapabilityName), -}).annotations({ identifier: 'NotionDatasourceSync.CapabilityPreflightResult' }) +}).annotate({ identifier: 'NotionDatasourceSync.CapabilityPreflightResult' }) export type CapabilityPreflightResult = typeof CapabilityPreflightResult.Type /** Observed write behavior for a Notion data-source property. */ -export const DataSourcePropertyWriteClass = Schema.Literal( +export const DataSourcePropertyWriteClass = Schema.Literals([ 'writable', 'computed', 'unsupported', -).annotations({ identifier: 'NotionDatasourceSync.DataSourcePropertyWriteClass' }) +]).annotate({ identifier: 'NotionDatasourceSync.DataSourcePropertyWriteClass' }) export type DataSourcePropertyWriteClass = typeof DataSourcePropertyWriteClass.Type /** Ordered typed schema descriptor observed from Notion's data-source properties map. */ export const DataSourcePropertySnapshot = Schema.TaggedStruct('DataSourcePropertySnapshot', { propertyId: PropertyId, name: PropertyName, - type: Schema.NonEmptyTrimmedString, + type: Schema.Trimmed.check(Schema.isNonEmpty()), configHash: Hash, writeClass: DataSourcePropertyWriteClass, - ordinal: Schema.NonNegativeInt, + ordinal: Schema.Natural, configJson: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.DataSourcePropertySnapshot' }) +}).annotate({ identifier: 'NotionDatasourceSync.DataSourcePropertySnapshot' }) export type DataSourcePropertySnapshot = typeof DataSourcePropertySnapshot.Type /** Point-in-time observation of a Notion database: captures request metadata plus independent schema and metadata hashes. */ @@ -225,7 +225,7 @@ export const DataSourceSnapshot = Schema.TaggedStruct('DataSourceSnapshot', { metadataJson: Schema.optional(Schema.String), metadataTitlePlainText: Schema.optional(Schema.String), metadataDescriptionPlainText: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.DataSourceSnapshot' }) +}).annotate({ identifier: 'NotionDatasourceSync.DataSourceSnapshot' }) export type DataSourceSnapshot = typeof DataSourceSnapshot.Type /** Point-in-time observation of a Notion UI view attached to a database/data source. */ @@ -236,10 +236,10 @@ export const DataSourceViewSnapshot = Schema.TaggedStruct('DataSourceViewSnapsho requestId: NotionRequestId, observedAt: Schema.DateTimeUtc, name: Schema.String, - viewType: Schema.NonEmptyTrimmedString, + viewType: Schema.Trimmed.check(Schema.isNonEmpty()), viewHash: Hash, viewJson: Schema.String, -}).annotations({ identifier: 'NotionDatasourceSync.DataSourceViewSnapshot' }) +}).annotate({ identifier: 'NotionDatasourceSync.DataSourceViewSnapshot' }) export type DataSourceViewSnapshot = typeof DataSourceViewSnapshot.Type /** Point-in-time observation of a Notion page: captures properties hash, trash state, and request metadata. */ @@ -249,18 +249,18 @@ export const PageSnapshot = Schema.TaggedStruct('PageSnapshot', { requestId: NotionRequestId, observedAt: Schema.DateTimeUtc, propertiesHash: Hash, - propertyValuesJson: Schema.optional(Schema.Record({ key: PropertyId, value: Schema.String })), + propertyValuesJson: Schema.optional(Schema.Record(PropertyId, Schema.String)), inTrash: Schema.Boolean, -}).annotations({ identifier: 'NotionDatasourceSync.PageSnapshot' }) +}).annotate({ identifier: 'NotionDatasourceSync.PageSnapshot' }) export type PageSnapshot = typeof PageSnapshot.Type /** Reason why a body block could not be fully represented during observation; drives body-safety guard decisions. */ -export const BodyUnknownBlockCause = Schema.Literal( +export const BodyUnknownBlockCause = Schema.Literals([ 'truncation', 'permission', 'unsupported', 'unknown', -).annotations({ identifier: 'NotionDatasourceSync.BodyUnknownBlockCause' }) +]).annotate({ identifier: 'NotionDatasourceSync.BodyUnknownBlockCause' }) export type BodyUnknownBlockCause = typeof BodyUnknownBlockCause.Type /** @@ -269,7 +269,7 @@ export type BodyUnknownBlockCause = typeof BodyUnknownBlockCause.Type * The `guardBodySafety` and `guardBodyAdapterBoundary` guards reject commands whose adapter * touched any surface other than `'body'`. */ -export const BodyAdapterMutationSurface = Schema.Literal( +export const BodyAdapterMutationSurface = Schema.Literals([ 'body', 'row-property', 'schema', @@ -279,25 +279,25 @@ export const BodyAdapterMutationSurface = Schema.Literal( 'cover', 'page-metadata', 'membership', -).annotations({ identifier: 'NotionDatasourceSync.BodyAdapterMutationSurface' }) +]).annotate({ identifier: 'NotionDatasourceSync.BodyAdapterMutationSurface' }) export type BodyAdapterMutationSurface = typeof BodyAdapterMutationSurface.Type /** Safety assessment of a page body at the time it was observed; the `guardBodySafety` guard consumes this to decide whether a push is safe. */ export const BodySafetySnapshot = Schema.Struct({ truncated: Schema.Boolean, unknownBlockCause: Schema.optional(BodyUnknownBlockCause), - selection: Schema.Literal('safe', 'ambiguous'), + selection: Schema.Literals(['safe', 'ambiguous']), wouldDeleteChildren: Schema.Boolean, syncedPageUnsupported: Schema.Boolean, adapterConflict: Schema.Boolean, adapterMutationSurfaces: Schema.Array(BodyAdapterMutationSurface), -}).annotations({ identifier: 'NotionDatasourceSync.BodySafetySnapshot' }) +}).annotate({ identifier: 'NotionDatasourceSync.BodySafetySnapshot' }) export type BodySafetySnapshot = typeof BodySafetySnapshot.Type /** Body identity derived only from rendered Markdown bytes. */ export const RenderedBodyIdentity = Schema.TaggedStruct('RenderedBodyIdentity', { rendered: ContentDescriptor, -}).annotations({ identifier: 'NotionDatasourceSync.RenderedBodyIdentity' }) +}).annotate({ identifier: 'NotionDatasourceSync.RenderedBodyIdentity' }) export type RenderedBodyIdentity = typeof RenderedBodyIdentity.Type /** Body identity derived from a full remote observation evidence envelope. */ @@ -305,14 +305,14 @@ export const EvidenceBackedBodyIdentity = Schema.TaggedStruct('EvidenceBackedBod evidenceFingerprint: BodyEvidenceFingerprint, rendered: ContentDescriptor, completeness: BodyCompletenessEvidence, -}).annotations({ identifier: 'NotionDatasourceSync.EvidenceBackedBodyIdentity' }) +}).annotate({ identifier: 'NotionDatasourceSync.EvidenceBackedBodyIdentity' }) export type EvidenceBackedBodyIdentity = typeof EvidenceBackedBodyIdentity.Type /** Typed page-body identity used for body stale-base guards, settlement, replay, and telemetry. */ -export const BodyIdentity = Schema.Union( +export const BodyIdentity = Schema.Union([ RenderedBodyIdentity, EvidenceBackedBodyIdentity, -).annotations({ +]).annotate({ identifier: 'NotionDatasourceSync.BodyIdentity', }) export type BodyIdentity = typeof BodyIdentity.Type @@ -323,7 +323,7 @@ export const BodyPointer = Schema.TaggedStruct('BodyPointer', { identity: BodyIdentity, observedAt: Schema.DateTimeUtc, safety: BodySafetySnapshot, -}).annotations({ identifier: 'NotionDatasourceSync.BodyPointer' }) +}).annotate({ identifier: 'NotionDatasourceSync.BodyPointer' }) export type BodyPointer = typeof BodyPointer.Type const decodeHash = Schema.decodeUnknownSync(Hash) @@ -408,7 +408,7 @@ export const RowPageSnapshot = Schema.TaggedStruct('RowPageSnapshot', { propertiesHash: Hash, lastEditedTime: Schema.DateTimeUtc, inTrash: Schema.Boolean, -}).annotations({ identifier: 'NotionDatasourceSync.RowPageSnapshot' }) +}).annotate({ identifier: 'NotionDatasourceSync.RowPageSnapshot' }) export type RowPageSnapshot = typeof RowPageSnapshot.Type /** A single item from a paginated property value retrieval; carries stable hash identifiers for change detection. */ @@ -418,7 +418,7 @@ export const PagePropertyItem = Schema.TaggedStruct('PagePropertyItem', { itemHash: Hash, valueHash: Hash, valueJson: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.PagePropertyItem' }) +}).annotate({ identifier: 'NotionDatasourceSync.PagePropertyItem' }) export type PagePropertyItem = typeof PagePropertyItem.Type /** @@ -433,20 +433,20 @@ export const LocalArtifactObservation = Schema.TaggedStruct('LocalArtifactObserv contentHash: Hash, bodyContent: Schema.optional(Schema.String), observedAt: Schema.DateTimeUtc, - state: Schema.Literal('present', 'delete-candidate'), + state: Schema.Literals(['present', 'delete-candidate']), ownWriteSuppressionToken: Schema.optional(OwnWriteSuppressionToken), -}).annotations({ identifier: 'NotionDatasourceSync.LocalArtifactObservation' }) +}).annotate({ identifier: 'NotionDatasourceSync.LocalArtifactObservation' }) export type LocalArtifactObservation = typeof LocalArtifactObservation.Type /** Intent to claim a workspace-relative path for a given page; submitted to `LocalWorkspacePort.claimPath`. */ export const PathClaimPlan = Schema.TaggedStruct('PathClaimPlan', { pageId: PageId, path: WorkspaceRelativePath, -}).annotations({ identifier: 'NotionDatasourceSync.PathClaimPlan' }) +}).annotate({ identifier: 'NotionDatasourceSync.PathClaimPlan' }) export type PathClaimPlan = typeof PathClaimPlan.Type /** Outcome of a path-claim attempt — either `'claimed'` (path is now owned by this page) or `'conflict'` (path already held by another page). */ -export const PathClaimResult = Schema.Union( +export const PathClaimResult = Schema.Union([ Schema.TaggedStruct('claimed', { pageId: PageId, path: WorkspaceRelativePath, @@ -456,7 +456,7 @@ export const PathClaimResult = Schema.Union( requestedPath: WorkspaceRelativePath, existingPageId: PageId, }), -).annotations({ identifier: 'NotionDatasourceSync.PathClaimResult' }) +]).annotate({ identifier: 'NotionDatasourceSync.PathClaimResult' }) export type PathClaimResult = typeof PathClaimResult.Type /** Plan for materializing a Notion page body as a local file at the specified path. */ @@ -473,10 +473,8 @@ export const MaterializePlan = Schema.TaggedStruct('MaterializePlan', { * that do not supply schema/cell evidence), which keeps the empty-`properties` * behavior. */ - writableProperties: Schema.optional( - Schema.Record({ key: Schema.String, value: NmdWritablePropertyValueSchema }), - ), -}).annotations({ identifier: 'NotionDatasourceSync.MaterializePlan' }) + writableProperties: Schema.optional(Schema.Record(Schema.String, NmdWritablePropertyValueSchema)), +}).annotate({ identifier: 'NotionDatasourceSync.MaterializePlan' }) export type MaterializePlan = typeof MaterializePlan.Type /** Result of a successful materialization; includes the body hash and own-write suppression token for the written file. */ @@ -485,5 +483,5 @@ export const MaterializeResult = Schema.TaggedStruct('MaterializeResult', { path: WorkspaceRelativePath, bodyHash: Hash, ownWriteSuppressionToken: OwnWriteSuppressionToken, -}).annotations({ identifier: 'NotionDatasourceSync.MaterializeResult' }) +}).annotate({ identifier: 'NotionDatasourceSync.MaterializeResult' }) export type MaterializeResult = typeof MaterializeResult.Type diff --git a/packages/@overeng/notion-datasource-sync/src/core/errors.ts b/packages/@overeng/notion-datasource-sync/src/core/errors.ts index 0a6b2c8900..cb1685f926 100644 --- a/packages/@overeng/notion-datasource-sync/src/core/errors.ts +++ b/packages/@overeng/notion-datasource-sync/src/core/errors.ts @@ -4,7 +4,7 @@ import { CommandId, DataSourceId, PageId, SupportedNotionApiVersion } from './do import { GuardName } from './guards.ts' /** Raised when the Notion API version is nominally supported but no compatibility proof has been recorded yet for this client version. */ -export class ApiVersionCompatibilityMissing extends Schema.TaggedError()( +export class ApiVersionCompatibilityMissing extends Schema.TaggedErrorClass()( 'ApiVersionCompatibilityMissing', { requestedVersion: Schema.String, @@ -14,7 +14,7 @@ export class ApiVersionCompatibilityMissing extends Schema.TaggedError()( +export class UnsupportedCapabilityError extends Schema.TaggedErrorClass()( 'UnsupportedCapabilityError', { dataSourceId: DataSourceId, @@ -24,7 +24,7 @@ export class UnsupportedCapabilityError extends Schema.TaggedError()( +export class NotionGatewayError extends Schema.TaggedErrorClass()( 'NotionGatewayError', { operation: Schema.String, @@ -32,32 +32,32 @@ export class NotionGatewayError extends Schema.TaggedError() pageId: Schema.optional(PageId), requestId: Schema.optional(Schema.String), guard: Schema.optional(GuardName), - retryAfterMillis: Schema.optional(Schema.NonNegativeInt), + retryAfterMillis: Schema.optional(Schema.Natural), message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** I/O or integrity error from the local event store or projection storage. */ -export class LocalStoreError extends Schema.TaggedError()('LocalStoreError', { +export class LocalStoreError extends Schema.TaggedErrorClass()('LocalStoreError', { operation: Schema.String, message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }) {} /** Alias for `LocalStoreError`; used in port signatures that deal with filesystem-level storage. */ export type LocalStorageError = LocalStoreError /** Error from the body-sync port (observe, push, or repair) for a specific page. */ -export class BodySyncError extends Schema.TaggedError()('BodySyncError', { +export class BodySyncError extends Schema.TaggedErrorClass()('BodySyncError', { operation: Schema.String, pageId: PageId, message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }) {} /** Raised when a sync guard blocks an operation that would normally be a defect; carries the guard name and optional command context. */ -export class SyncGuardError extends Schema.TaggedError()('SyncGuardError', { +export class SyncGuardError extends Schema.TaggedErrorClass()('SyncGuardError', { guard: GuardName, commandId: Schema.optional(CommandId), message: Schema.String, @@ -71,14 +71,14 @@ export class SyncGuardError extends Schema.TaggedError()('SyncGu * rewrites, or reinterprets local artifacts under an unrecognized or * inconsistent namespace. */ -export class WorkspaceNamespaceError extends Schema.TaggedError()( +export class WorkspaceNamespaceError extends Schema.TaggedErrorClass()( 'WorkspaceNamespaceError', { - guard: Schema.Literal( + guard: Schema.Literals([ 'UnknownWorkspaceNamespace', 'MixedWorkspaceNamespace', 'InvalidLinkedView', - ), + ]), message: Schema.String, }, ) {} @@ -89,7 +89,7 @@ export class WorkspaceNamespaceError extends Schema.TaggedError()( +export class WorkspaceNotTracked extends Schema.TaggedErrorClass()( 'WorkspaceNotTracked', { message: Schema.String, diff --git a/packages/@overeng/notion-datasource-sync/src/core/events.ts b/packages/@overeng/notion-datasource-sync/src/core/events.ts index 246cbb22f1..6cc62e08b6 100644 --- a/packages/@overeng/notion-datasource-sync/src/core/events.ts +++ b/packages/@overeng/notion-datasource-sync/src/core/events.ts @@ -20,27 +20,27 @@ import { import { GuardName } from './guards.ts' /** Branded unique identifier for a single event in the sync event log. */ -export const SyncEventId = Schema.NonEmptyTrimmedString.pipe( +export const SyncEventId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.SyncEventId'), - Schema.annotations({ identifier: 'NotionDatasourceSync.SyncEventId' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.SyncEventId' }), ) export type SyncEventId = typeof SyncEventId.Type /** Branded identifier for a sync root (a single data-source ↔ workspace binding); partitions the event log. */ -export const SyncRootId = Schema.NonEmptyTrimmedString.pipe( +export const SyncRootId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.SyncRootId'), - Schema.annotations({ identifier: 'NotionDatasourceSync.SyncRootId' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.SyncRootId' }), ) export type SyncRootId = typeof SyncRootId.Type /** Schema version of the event envelope codec; bumped when the envelope shape changes in a breaking way. */ -export const EventCodecVersion = Schema.Literal('v1').annotations({ +export const EventCodecVersion = Schema.Literal('v1').annotate({ identifier: 'NotionDatasourceSync.EventCodecVersion', }) export type EventCodecVersion = typeof EventCodecVersion.Type /** High-level classification of a sync event; used for filtering and projection without needing to decode the full payload. */ -export const EventFamily = Schema.Literal( +export const EventFamily = Schema.Literals([ 'RemoteObserved', 'SyncRootBound', 'CompatibilityChecked', @@ -55,20 +55,20 @@ export const EventFamily = Schema.Literal( 'GuardBlocked', 'RepairObserved', 'StorageMigrated', -).annotations({ identifier: 'NotionDatasourceSync.EventFamily' }) +]).annotate({ identifier: 'NotionDatasourceSync.EventFamily' }) export type EventFamily = typeof EventFamily.Type /** Branded key that ensures a command or event is applied at most once even if retried. */ -export const IdempotencyKey = Schema.NonEmptyTrimmedString.pipe( +export const IdempotencyKey = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.IdempotencyKey'), - Schema.annotations({ identifier: 'NotionDatasourceSync.IdempotencyKey' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.IdempotencyKey' }), ) export type IdempotencyKey = typeof IdempotencyKey.Type /** Branded composite key identifying the sync surface an event or conflict applies to (e.g. `page::body`). */ -export const SurfaceKey = Schema.NonEmptyTrimmedString.pipe( +export const SurfaceKey = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.SurfaceKey'), - Schema.annotations({ identifier: 'NotionDatasourceSync.SurfaceKey' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.SurfaceKey' }), ) export type SurfaceKey = typeof SurfaceKey.Type @@ -76,7 +76,7 @@ export type SurfaceKey = typeof SurfaceKey.Type export const VersionedJson = Schema.TaggedStruct('VersionedJson', { codecVersion: EventCodecVersion, canonicalJson: Schema.String, -}).annotations({ identifier: 'NotionDatasourceSync.VersionedJson' }) +}).annotate({ identifier: 'NotionDatasourceSync.VersionedJson' }) export type VersionedJson = typeof VersionedJson.Type /** Returns the common envelope field schemas shared by every sync event; spread into each concrete event struct. */ @@ -90,7 +90,7 @@ export const eventEnvelopeFields = () {} +export class NotionDataSourceGateway extends Context.Service< + NotionDataSourceGateway, + NotionDataSourceGatewayShape +>()('@overeng/notion-datasource-sync/NotionDataSourceGateway') {} /** Contract for the body-sync adapter: observe, plan local changes against, push, and repair the markdown body of a Notion page. */ export type PageBodySyncPortShape = { @@ -107,9 +108,9 @@ export type PageBodySyncPortShape = { } /** Effect service tag for the page body sync adapter; implementations translate between `BodyPointer` and the actual file/Notion content representation. */ -export class PageBodySyncPort extends Context.Tag( +export class PageBodySyncPort extends Context.Service()( '@overeng/notion-datasource-sync/PageBodySyncPort', -)() {} +) {} /** Contract for local filesystem interactions: scan for artifact observations, claim paths for pages, and materialize body content as files. */ export type LocalWorkspacePortShape = { @@ -121,9 +122,10 @@ export type LocalWorkspacePortShape = { } /** Effect service tag for the local workspace adapter; inject to connect the sync core to the real filesystem implementation. */ -export class LocalWorkspacePort extends Context.Tag( - '@overeng/notion-datasource-sync/LocalWorkspacePort', -)() {} +export class LocalWorkspacePort extends Context.Service< + LocalWorkspacePort, + LocalWorkspacePortShape +>()('@overeng/notion-datasource-sync/LocalWorkspacePort') {} /** Contract for the append-only sync event store: `append` persists new events and `replay` streams all recorded events for projection rebuilding. */ export type SyncEventStoreShape = { @@ -132,7 +134,6 @@ export type SyncEventStoreShape = { } /** Effect service tag for the sync event store; the store is the authoritative source of truth for all sync state. */ -export class SyncEventStore extends Context.Tag('@overeng/notion-datasource-sync/SyncEventStore')< - SyncEventStore, - SyncEventStoreShape ->() {} +export class SyncEventStore extends Context.Service()( + '@overeng/notion-datasource-sync/SyncEventStore', +) {} diff --git a/packages/@overeng/notion-datasource-sync/src/core/progress.ts b/packages/@overeng/notion-datasource-sync/src/core/progress.ts index af6a53e977..9100c49e0d 100644 --- a/packages/@overeng/notion-datasource-sync/src/core/progress.ts +++ b/packages/@overeng/notion-datasource-sync/src/core/progress.ts @@ -1,7 +1,7 @@ import { Context, Effect, Option, Schema } from 'effect' /** Coarse operation phases emitted by sync code for CLI/TUI progress. */ -export const SyncProgressPhase = Schema.Literal( +export const SyncProgressPhase = Schema.Literals([ 'preparing', 'pulling', 'querying', @@ -12,7 +12,7 @@ export const SyncProgressPhase = Schema.Literal( 'projecting', 'watching', 'complete', -) +]) export type SyncProgressPhase = typeof SyncProgressPhase.Type @@ -57,10 +57,9 @@ export type SyncProgressReporter = { } /** Optional Effect service used by CLI/TUI surfaces to observe sync progress. */ -export class SyncProgress extends Context.Tag('@overeng/notion-datasource-sync/SyncProgress')< - SyncProgress, - SyncProgressReporter ->() {} +export class SyncProgress extends Context.Service()( + '@overeng/notion-datasource-sync/SyncProgress', +) {} /** Emits a sync progress event when the optional progress service is available. */ export const reportSyncProgress = (event: SyncProgressEvent): Effect.Effect => diff --git a/packages/@overeng/notion-datasource-sync/src/core/signals.ts b/packages/@overeng/notion-datasource-sync/src/core/signals.ts index 0af2107580..9fdcde1d51 100644 --- a/packages/@overeng/notion-datasource-sync/src/core/signals.ts +++ b/packages/@overeng/notion-datasource-sync/src/core/signals.ts @@ -4,40 +4,40 @@ import { DataSourceId, PageId } from './domain.ts' import { SyncRootId } from './events.ts' /** Stable local identifier for a durable wake signal in the SQLite inbox. */ -export const SignalId = Schema.NonEmptyTrimmedString.pipe( +export const SignalId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.SignalId'), - Schema.annotations({ identifier: 'NotionDatasourceSync.SignalId' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.SignalId' }), ) export type SignalId = typeof SignalId.Type /** Provider or transport that delivered the signal, for example a webhook bridge or manual test source. */ -export const SignalProvider = Schema.NonEmptyTrimmedString.pipe( +export const SignalProvider = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.SignalProvider'), - Schema.annotations({ identifier: 'NotionDatasourceSync.SignalProvider' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.SignalProvider' }), ) export type SignalProvider = typeof SignalProvider.Type /** Provider-scoped idempotency key for deduping repeated signal deliveries. */ -export const SignalExternalId = Schema.NonEmptyTrimmedString.pipe( +export const SignalExternalId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('NotionDatasourceSync.SignalExternalId'), - Schema.annotations({ identifier: 'NotionDatasourceSync.SignalExternalId' }), + Schema.annotate({ identifier: 'NotionDatasourceSync.SignalExternalId' }), ) export type SignalExternalId = typeof SignalExternalId.Type /** Stage-1 signals only wake the existing full sync path; payload hints are stored but not used for targeted pulls. */ -export const SignalKind = Schema.Literal('remote-change').annotations({ +export const SignalKind = Schema.Literal('remote-change').annotate({ identifier: 'NotionDatasourceSync.SignalKind', }) export type SignalKind = typeof SignalKind.Type /** Durable inbox lifecycle for signal processing. */ -export const SignalState = Schema.Literal('pending', 'claimed', 'processed', 'failed').annotations({ +export const SignalState = Schema.Literals(['pending', 'claimed', 'processed', 'failed']).annotate({ identifier: 'NotionDatasourceSync.SignalState', }) export type SignalState = typeof SignalState.Type /** JSON payload captured from the provider. It is deliberately opaque to keep the inbox provider-neutral. */ -export const SignalPayloadJson = Schema.String.annotations({ +export const SignalPayloadJson = Schema.String.annotate({ identifier: 'NotionDatasourceSync.SignalPayloadJson', }) export type SignalPayloadJson = typeof SignalPayloadJson.Type @@ -53,23 +53,23 @@ export const SignalInboxRecord = Schema.Struct({ state: SignalState, dataSourceId: Schema.optional(DataSourceId), pageId: Schema.optional(PageId), - attemptCount: Schema.NonNegativeInt, + attemptCount: Schema.Natural, leaseToken: Schema.optional(Schema.String), claimedAt: Schema.optional(Schema.String), processedAt: Schema.optional(Schema.String), lastError: Schema.optional(Schema.String), createdAt: Schema.String, updatedAt: Schema.String, -}).annotations({ identifier: 'NotionDatasourceSync.SignalInboxRecord' }) +}).annotate({ identifier: 'NotionDatasourceSync.SignalInboxRecord' }) export type SignalInboxRecord = typeof SignalInboxRecord.Type /** Aggregated signal inbox counters used by daemon wake decisions and health/status reporting. */ export const SignalInboxStatus = Schema.Struct({ - pending: Schema.NonNegativeInt, - claimed: Schema.NonNegativeInt, - processed: Schema.NonNegativeInt, - failed: Schema.NonNegativeInt, -}).annotations({ identifier: 'NotionDatasourceSync.SignalInboxStatus' }) + pending: Schema.Natural, + claimed: Schema.Natural, + processed: Schema.Natural, + failed: Schema.Natural, +}).annotate({ identifier: 'NotionDatasourceSync.SignalInboxStatus' }) export type SignalInboxStatus = typeof SignalInboxStatus.Type /** Input for inserting a durable remote-change wake signal into the sync store. */ diff --git a/packages/@overeng/notion-datasource-sync/src/daemon/watch.ts b/packages/@overeng/notion-datasource-sync/src/daemon/watch.ts index f98d3b6b5b..50da89c88e 100644 --- a/packages/@overeng/notion-datasource-sync/src/daemon/watch.ts +++ b/packages/@overeng/notion-datasource-sync/src/daemon/watch.ts @@ -165,7 +165,7 @@ export type WatchDaemonOptions = { } /** Tagged error raised when an `AbortSignal` fires mid-cycle, allowing the daemon loop to exit cleanly. */ -export class WatchDaemonCancelled extends Schema.TaggedError()( +export class WatchDaemonCancelled extends Schema.TaggedErrorClass()( 'WatchDaemonCancelled', { rootId: Schema.String, @@ -175,7 +175,7 @@ export class WatchDaemonCancelled extends Schema.TaggedError()( +export class WatchDaemonCycleTimedOut extends Schema.TaggedErrorClass()( 'WatchDaemonCycleTimedOut', { rootId: Schema.String, @@ -192,20 +192,22 @@ const WatchDaemonStateSchema = Schema.Struct({ lastCompleteCycle: Schema.Number, lastStartedAt: Schema.optional(Schema.String), lastCompletedAt: Schema.optional(Schema.String), - repair: Schema.Union( + repair: Schema.Union([ Schema.TaggedStruct('none', {}), Schema.TaggedStruct('retry', { reason: Schema.String, retryAfterMillis: Schema.Number, failedCycle: Schema.Number, }), - ), + ]), lastStatus: Schema.optional(Schema.Unknown), -}).annotations({ identifier: 'NotionDatasourceSync.WatchDaemonState' }) +}).annotate({ identifier: 'NotionDatasourceSync.WatchDaemonState' }) const decodeState = Schema.decodeUnknownSync(WatchDaemonStateSchema) -const decodeStateJson = Schema.decodeUnknownSync(Schema.parseJson(WatchDaemonStateSchema)) -const encodeStateJson = Schema.encodeSync(Schema.parseJson(WatchDaemonStateSchema, { space: 2 })) +const decodeStateJson = Schema.decodeUnknownSync(Schema.fromJsonString(WatchDaemonStateSchema)) +const encodeStateJson = Schema.encodeSync( + Schema.fromJsonString(WatchDaemonStateSchema, { space: 2 }), +) const modeBackoffMillis = (mode: WatchDaemonMode): number => { switch (mode) { diff --git a/packages/@overeng/notion-datasource-sync/src/demo/live-demo.ts b/packages/@overeng/notion-datasource-sync/src/demo/live-demo.ts index 32e8ea443c..4aa332f4a9 100644 --- a/packages/@overeng/notion-datasource-sync/src/demo/live-demo.ts +++ b/packages/@overeng/notion-datasource-sync/src/demo/live-demo.ts @@ -1,41 +1,43 @@ import { Either, Schema } from 'effect' /** Stable keys for the durable Notion datasource-sync demo domains. */ -export const NotionDatasourceSyncDemoDataSourceKey = Schema.Literal( +export const NotionDatasourceSyncDemoDataSourceKey = Schema.Literals([ 'projects', 'incidents', 'customers', 'activity', -) +]) export type NotionDatasourceSyncDemoDataSourceKey = typeof NotionDatasourceSyncDemoDataSourceKey.Type /** Separates read-only verification from the guarded public synthetic fixture provisioner. */ -export const NotionDatasourceSyncLiveFixtureLane = Schema.Literal( +export const NotionDatasourceSyncLiveFixtureLane = Schema.Literals([ 'read-only-verifier', 'provisioner', -) +]) export type NotionDatasourceSyncLiveFixtureLane = typeof NotionDatasourceSyncLiveFixtureLane.Type -const NotionId = Schema.String.pipe( - Schema.pattern(/^[0-9a-f]{32}$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i), +const NotionId = Schema.String.check( + Schema.isPattern( + /^[0-9a-f]{32}$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, + ), ) -const NotionUrl = Schema.String.pipe(Schema.pattern(/^https:\/\/www\.notion\.so\//)) +const NotionUrl = Schema.String.check(Schema.isPattern(/^https:\/\/www\.notion\.so\//)) /** Expected live Notion database/data-source shape for one demo domain. */ export const NotionDatasourceSyncDemoDataSourceSchema = Schema.Struct({ key: NotionDatasourceSyncDemoDataSourceKey, - title: Schema.NonEmptyTrimmedString, + title: Schema.Trimmed.check(Schema.isNonEmpty()), databaseId: NotionId, databaseUrl: NotionUrl, dataSourceId: NotionId, expectedRows: Schema.Number, - expectedPropertyNames: Schema.Array(Schema.NonEmptyTrimmedString), + expectedPropertyNames: Schema.Array(Schema.Trimmed.check(Schema.isNonEmpty())), fastReplica: Schema.Boolean, -}).annotations({ identifier: 'NotionDatasourceSync.DemoDataSource' }) +}).annotate({ identifier: 'NotionDatasourceSync.DemoDataSource' }) export type NotionDatasourceSyncDemoDataSource = typeof NotionDatasourceSyncDemoDataSourceSchema.Type @@ -46,8 +48,8 @@ export const NotionDatasourceSyncDemoProvisionerContractSchema = Schema.Struct({ ownedBy: Schema.Literal('notion-datasource-sync-demo-provisioner'), writes: Schema.Literal('public-synthetic-fixtures-only'), emittedIds: Schema.Literal('env-or-public-synthetic-manifest'), - requiredMarker: Schema.NonEmptyTrimmedString, -}).annotations({ identifier: 'NotionDatasourceSync.DemoProvisionerContract' }) + requiredMarker: Schema.Trimmed.check(Schema.isNonEmpty()), +}).annotate({ identifier: 'NotionDatasourceSync.DemoProvisionerContract' }) /** Durable online demo page plus every child data source the package verifies. */ export const NotionDatasourceSyncDemoManifestSchema = Schema.Struct({ @@ -62,7 +64,7 @@ export const NotionDatasourceSyncDemoManifestSchema = Schema.Struct({ }), provisionerContract: NotionDatasourceSyncDemoProvisionerContractSchema, dataSources: Schema.Array(NotionDatasourceSyncDemoDataSourceSchema), -}).annotations({ identifier: 'NotionDatasourceSync.DemoManifest' }) +}).annotate({ identifier: 'NotionDatasourceSync.DemoManifest' }) export type NotionDatasourceSyncDemoManifest = typeof NotionDatasourceSyncDemoManifestSchema.Type @@ -78,13 +80,15 @@ const NotionApiFailureBodySchema = Schema.Struct({ status: Schema.optional(Schema.Number), code: Schema.optional(Schema.String), message: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.DemoApiFailureBody' }) +}).annotate({ identifier: 'NotionDatasourceSync.DemoApiFailureBody' }) const decodeNotionApiFailureBody = ( body: string | undefined, ): typeof NotionApiFailureBodySchema.Type | undefined => { if (body === undefined) return undefined - const decoded = Schema.decodeUnknownEither(Schema.parseJson(NotionApiFailureBodySchema))(body) + const decoded = Schema.decodeUnknownEither(Schema.fromJsonString(NotionApiFailureBodySchema))( + body, + ) return Either.isRight(decoded) === true ? decoded.right : undefined } diff --git a/packages/@overeng/notion-datasource-sync/src/e2e/body-adapter.e2e.test.ts b/packages/@overeng/notion-datasource-sync/src/e2e/body-adapter.e2e.test.ts index f77a1a4c55..7adc8904e7 100644 --- a/packages/@overeng/notion-datasource-sync/src/e2e/body-adapter.e2e.test.ts +++ b/packages/@overeng/notion-datasource-sync/src/e2e/body-adapter.e2e.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer, Schema } from 'effect' import { describe, expect, it } from 'vitest' @@ -178,7 +178,7 @@ const runWithNmdStateStore = ( effect: Effect.Effect, ) => Effect.runPromise( - effect.pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)))), + effect.pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)))), ) const assertNoGatewayMutations = (ledger: ReturnType['ledger']) => { @@ -1068,7 +1068,7 @@ describe('body adapter E2E boundary', () => { statusFile({ path: absoluteBodyPath }).pipe( Effect.provideService(NotionMdGateway, notionMdGateway), Effect.provideService(NmdStateStore, stateStore), - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), ), ) expect(nmdStatus.status).toBe('in-sync') diff --git a/packages/@overeng/notion-datasource-sync/src/e2e/cli.e2e.test.ts b/packages/@overeng/notion-datasource-sync/src/e2e/cli.e2e.test.ts index b156d0742e..ab419bf9da 100644 --- a/packages/@overeng/notion-datasource-sync/src/e2e/cli.e2e.test.ts +++ b/packages/@overeng/notion-datasource-sync/src/e2e/cli.e2e.test.ts @@ -6,7 +6,7 @@ import { DatabaseSync } from 'node:sqlite' import { fileURLToPath } from 'node:url' import { promisify } from 'node:util' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Cause, Effect, Exit, Layer, Option, Schema, Stream } from 'effect' import { describe, expect, it } from 'vitest' @@ -317,7 +317,7 @@ const runWithNmdStateStore = ( effect: Effect.Effect, ) => Effect.runPromise( - effect.pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)))), + effect.pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)))), ) const createBoundSqlite = async ({ @@ -1040,8 +1040,8 @@ describe('CLI command surface', () => { ) expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit) === true) { - const failure = Cause.failureOption(exit.cause) - // A defect would land in `Cause.defects`, not `Cause.failureOption`. + const failure = Cause.findErrorOption(exit.cause) + // A defect would land in `Cause.defects`, not `Cause.findErrorOption`. expect(Option.isSome(failure)).toBe(true) expect(Cause.defects(exit.cause)).toHaveLength(0) if (Option.isSome(failure) === true) { diff --git a/packages/@overeng/notion-datasource-sync/src/e2e/live-notion.e2e.test.ts b/packages/@overeng/notion-datasource-sync/src/e2e/live-notion.e2e.test.ts index d1b774250d..b9d5ff3d96 100644 --- a/packages/@overeng/notion-datasource-sync/src/e2e/live-notion.e2e.test.ts +++ b/packages/@overeng/notion-datasource-sync/src/e2e/live-notion.e2e.test.ts @@ -4,8 +4,8 @@ import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { DatabaseSync } from 'node:sqlite' -import { FetchHttpClient, type HttpClient } from '@effect/platform' import { Chunk, Effect, Layer, Option, Redacted, Schema, Stream } from 'effect' +import { FetchHttpClient, type HttpClient } from 'effect/unstable/http' import { describe, expect, it } from 'vitest' import { diff --git a/packages/@overeng/notion-datasource-sync/src/e2e/property-materialization-production.e2e.test.ts b/packages/@overeng/notion-datasource-sync/src/e2e/property-materialization-production.e2e.test.ts index a48f3c4c80..10d5865c95 100644 --- a/packages/@overeng/notion-datasource-sync/src/e2e/property-materialization-production.e2e.test.ts +++ b/packages/@overeng/notion-datasource-sync/src/e2e/property-materialization-production.e2e.test.ts @@ -22,7 +22,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Chunk, Effect, Layer, Stream } from 'effect' import { afterEach, describe, expect, it } from 'vitest' @@ -298,7 +298,7 @@ const observeAndMaterialize = async ({ Effect.provideService(PageBodySyncPort, bodyPort()), Effect.provideService(LocalWorkspacePort, workspace), ) - }).pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)))), + }).pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)))), ) return { nmdPath: join(root, bodyPathForPageInSource()), events: result.events } @@ -473,7 +473,7 @@ describe('SM5d property materialization (real pull → materialized .nmd frontma return yield* workspace .scan(root) .pipe(Stream.runCollect, Effect.map(Chunk.toReadonlyArray)) - }).pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)))), + }).pipe(Effect.provide(NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)))), ) const observation = observations.find((o) => o.pageId === pageUuid) diff --git a/packages/@overeng/notion-datasource-sync/src/e2e/realistic-workflows.e2e.test.ts b/packages/@overeng/notion-datasource-sync/src/e2e/realistic-workflows.e2e.test.ts index 9e52c214f0..bf5b149f52 100644 --- a/packages/@overeng/notion-datasource-sync/src/e2e/realistic-workflows.e2e.test.ts +++ b/packages/@overeng/notion-datasource-sync/src/e2e/realistic-workflows.e2e.test.ts @@ -1,6 +1,6 @@ import { join } from 'node:path' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { NodeFileSystem } from '@effect/platform-node' import { Effect, Schema } from 'effect' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-datasource-sync/src/gateway/fake.ts b/packages/@overeng/notion-datasource-sync/src/gateway/fake.ts index 4ca3a8966a..5af918027d 100644 --- a/packages/@overeng/notion-datasource-sync/src/gateway/fake.ts +++ b/packages/@overeng/notion-datasource-sync/src/gateway/fake.ts @@ -774,7 +774,7 @@ export const makeFakeNotionDataSourceGateway = ( descriptionPlainText: snapshot.metadataDescriptionPlainText ?? '', icon: { _tag: 'none' }, } satisfies CanonicalDataSourceMetadata) - : Schema.decodeUnknownSync(Schema.parseJson(CanonicalDataSourceMetadata))( + : Schema.decodeUnknownSync(Schema.fromJsonString(CanonicalDataSourceMetadata))( snapshot.metadataJson, ) const nextMetadata: CanonicalDataSourceMetadata = { @@ -836,7 +836,7 @@ export const makeFakeNotionDataSourceGateway = ( descriptionPlainText: snapshot.metadataDescriptionPlainText ?? '', icon: { _tag: 'none' }, } satisfies CanonicalDataSourceMetadata) - : Schema.decodeUnknownSync(Schema.parseJson(CanonicalDataSourceMetadata))( + : Schema.decodeUnknownSync(Schema.fromJsonString(CanonicalDataSourceMetadata))( snapshot.metadataJson, ) const nextMetadata: CanonicalDataSourceMetadata = { diff --git a/packages/@overeng/notion-datasource-sync/src/gateway/notion.ts b/packages/@overeng/notion-datasource-sync/src/gateway/notion.ts index dff043f7b2..5f556529b0 100644 --- a/packages/@overeng/notion-datasource-sync/src/gateway/notion.ts +++ b/packages/@overeng/notion-datasource-sync/src/gateway/notion.ts @@ -1,5 +1,5 @@ -import { HttpClient } from '@effect/platform' import { Context, Effect, Layer, Option, Schema, type Scope, Stream } from 'effect' +import { HttpClient } from 'effect/unstable/http' import { type DatabaseFilter, @@ -177,7 +177,7 @@ export type NotionGatewayClient = { } /** Tagged error raised when the live gateway is asked to perform an operation it cannot map onto the underlying Notion client. */ -export class UnsupportedAdapterOperation extends Schema.TaggedError()( +export class UnsupportedAdapterOperation extends Schema.TaggedErrorClass()( 'UnsupportedAdapterOperation', { operation: Schema.String, diff --git a/packages/@overeng/notion-datasource-sync/src/local/manifest.ts b/packages/@overeng/notion-datasource-sync/src/local/manifest.ts index 24d2e84266..48335207e3 100644 --- a/packages/@overeng/notion-datasource-sync/src/local/manifest.ts +++ b/packages/@overeng/notion-datasource-sync/src/local/manifest.ts @@ -42,7 +42,7 @@ export const hiddenStateDirectoryName = join('.notion', NAMESPACE_VERSION) * Authority mode recorded in the manifest. Governs whether local edits, remote * state, or a converged shared view is authoritative for a workspace. */ -export const AuthorityMode = Schema.Literal('local', 'remote', 'shared').annotations({ +export const AuthorityMode = Schema.Literals(['local', 'remote', 'shared']).annotate({ identifier: 'NotionDatasourceSync.AuthorityMode', }) export type AuthorityMode = typeof AuthorityMode.Type @@ -50,14 +50,14 @@ export type AuthorityMode = typeof AuthorityMode.Type /** One tracked data source entry in the workspace manifest. */ export const WorkspaceManifestDataSourceV1 = Schema.Struct({ /** Stable workspace-local name for the source; doubles as the data-file/page-dir stem. */ - name: Schema.NonEmptyTrimmedString, + name: Schema.Trimmed.check(Schema.isNonEmpty()), data_source_id: DataSourceId, - database_id: Schema.NonEmptyTrimmedString, + database_id: Schema.Trimmed.check(Schema.isNonEmpty()), /** Workspace-relative path to the source's SQLite data file, e.g. `data/v1/.sqlite`. */ - data_file: Schema.NonEmptyTrimmedString, + data_file: Schema.Trimmed.check(Schema.isNonEmpty()), /** Workspace-relative path to the source's page directory, e.g. `pages/v1/`. */ - pages_dir: Schema.NonEmptyTrimmedString, -}).annotations({ identifier: 'NotionDatasourceSync.WorkspaceManifestDataSourceV1' }) + pages_dir: Schema.Trimmed.check(Schema.isNonEmpty()), +}).annotate({ identifier: 'NotionDatasourceSync.WorkspaceManifestDataSourceV1' }) export type WorkspaceManifestDataSourceV1 = typeof WorkspaceManifestDataSourceV1.Type /** @@ -66,11 +66,11 @@ export type WorkspaceManifestDataSourceV1 = typeof WorkspaceManifestDataSourceV1 * contexts over a tracked `data_source_id`. */ export const WorkspaceManifestLinkedViewV1 = Schema.Struct({ - name: Schema.NonEmptyTrimmedString, - view_id: Schema.NonEmptyTrimmedString, + name: Schema.Trimmed.check(Schema.isNonEmpty()), + view_id: Schema.Trimmed.check(Schema.isNonEmpty()), data_source_id: DataSourceId, mode: Schema.Literal('projection'), -}).annotations({ identifier: 'NotionDatasourceSync.WorkspaceManifestLinkedViewV1' }) +}).annotate({ identifier: 'NotionDatasourceSync.WorkspaceManifestLinkedViewV1' }) export type WorkspaceManifestLinkedViewV1 = typeof WorkspaceManifestLinkedViewV1.Type /** @@ -86,7 +86,7 @@ export const WorkspaceManifestV1 = Schema.Struct({ authority_mode: AuthorityMode, data_sources: Schema.Array(WorkspaceManifestDataSourceV1), linked_views: Schema.optional(Schema.Array(WorkspaceManifestLinkedViewV1)), -}).annotations({ identifier: 'NotionDatasourceSync.WorkspaceManifestV1' }) +}).annotate({ identifier: 'NotionDatasourceSync.WorkspaceManifestV1' }) export type WorkspaceManifestV1 = typeof WorkspaceManifestV1.Type const decode = ({ diff --git a/packages/@overeng/notion-datasource-sync/src/local/sidecar.ts b/packages/@overeng/notion-datasource-sync/src/local/sidecar.ts index ebcd289e26..186f4e49cf 100644 --- a/packages/@overeng/notion-datasource-sync/src/local/sidecar.ts +++ b/packages/@overeng/notion-datasource-sync/src/local/sidecar.ts @@ -87,7 +87,7 @@ export const FilesystemWorkspaceSidecar = Schema.Struct({ materializedContentHash: Hash, ownWriteSuppressionToken: OwnWriteSuppressionToken, observedAt: Schema.String, -}).annotations({ identifier: 'NotionDatasourceSync.FilesystemWorkspaceSidecar' }) +}).annotate({ identifier: 'NotionDatasourceSync.FilesystemWorkspaceSidecar' }) export type FilesystemWorkspaceSidecar = typeof FilesystemWorkspaceSidecar.Type /** Constructs a sidecar record, deriving the own-write suppression token from `pageId`/`path`/`bodyHash` and stamping `observedAt` (defaults to now). */ diff --git a/packages/@overeng/notion-datasource-sync/src/local/workspace.ts b/packages/@overeng/notion-datasource-sync/src/local/workspace.ts index 94df1c9a76..e088479270 100644 --- a/packages/@overeng/notion-datasource-sync/src/local/workspace.ts +++ b/packages/@overeng/notion-datasource-sync/src/local/workspace.ts @@ -293,10 +293,10 @@ const observedAtNow = () => decode({ schema: Schema.DateTimeUtc, value: new Date const FilesystemPathClaim = Schema.Struct({ pageId: PageId, path: WorkspaceRelativePath, -}).annotations({ identifier: 'NotionDatasourceSync.FilesystemPathClaim' }) +}).annotate({ identifier: 'NotionDatasourceSync.FilesystemPathClaim' }) type FilesystemPathClaim = typeof FilesystemPathClaim.Type -const FilesystemPathClaims = Schema.Array(FilesystemPathClaim).annotations({ +const FilesystemPathClaims = Schema.Array(FilesystemPathClaim).annotate({ identifier: 'NotionDatasourceSync.FilesystemPathClaims', }) diff --git a/packages/@overeng/notion-datasource-sync/src/observability/observability.ts b/packages/@overeng/notion-datasource-sync/src/observability/observability.ts index 89bbb14048..980a8cb6d5 100644 --- a/packages/@overeng/notion-datasource-sync/src/observability/observability.ts +++ b/packages/@overeng/notion-datasource-sync/src/observability/observability.ts @@ -71,7 +71,7 @@ type SpanAttributesWithLabel = SpanAttributesInput & { /** Identifies the kind of process emitting a span, recorded on `spanAttr.processRole`. */ export type ProcessRole = 'cli' | 'daemon' | 'fake-gateway' | 'library' -const SpanAttributeValueSchema = Schema.Union(Schema.String, Schema.Number, Schema.Boolean) +const SpanAttributeValueSchema = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]) const optionalAttr = (key: SpanAttributeKey) => Schema.optional(SpanAttributeValueSchema.pipe(OtelAttr.key({ key }))) @@ -152,24 +152,16 @@ export const spanContracts = Object.fromEntries( } const StatusSpanAttributesSchema = Schema.Struct({ - state: Schema.Literal('clean', 'pending', 'conflict', 'blocked').pipe( + state: Schema.Literals(['clean', 'pending', 'conflict', 'blocked']).pipe( OtelAttr.key({ key: spanAttr.statusState }), ), - blockedCount: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: spanAttr.blockedCount })), - conflictCount: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: spanAttr.conflictCount })), - outboxAmbiguousCount: Schema.NonNegativeInt.pipe( - OtelAttr.key({ key: spanAttr.outboxAmbiguousCount }), - ), - outboxBlockedCount: Schema.NonNegativeInt.pipe( - OtelAttr.key({ key: spanAttr.outboxBlockedCount }), - ), - outboxQueuedCount: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: spanAttr.outboxQueuedCount })), - outboxRetryableCount: Schema.NonNegativeInt.pipe( - OtelAttr.key({ key: spanAttr.outboxRetryableCount }), - ), - outboxRunningCount: Schema.NonNegativeInt.pipe( - OtelAttr.key({ key: spanAttr.outboxRunningCount }), - ), + blockedCount: Schema.Natural.pipe(OtelAttr.key({ key: spanAttr.blockedCount })), + conflictCount: Schema.Natural.pipe(OtelAttr.key({ key: spanAttr.conflictCount })), + outboxAmbiguousCount: Schema.Natural.pipe(OtelAttr.key({ key: spanAttr.outboxAmbiguousCount })), + outboxBlockedCount: Schema.Natural.pipe(OtelAttr.key({ key: spanAttr.outboxBlockedCount })), + outboxQueuedCount: Schema.Natural.pipe(OtelAttr.key({ key: spanAttr.outboxQueuedCount })), + outboxRetryableCount: Schema.Natural.pipe(OtelAttr.key({ key: spanAttr.outboxRetryableCount })), + outboxRunningCount: Schema.Natural.pipe(OtelAttr.key({ key: spanAttr.outboxRunningCount })), }) /** Schema-backed contract for status summary attributes emitted on sync result spans. */ diff --git a/packages/@overeng/notion-datasource-sync/src/replica/replica.ts b/packages/@overeng/notion-datasource-sync/src/replica/replica.ts index e18de75edd..40ea18f042 100644 --- a/packages/@overeng/notion-datasource-sync/src/replica/replica.ts +++ b/packages/@overeng/notion-datasource-sync/src/replica/replica.ts @@ -43,7 +43,7 @@ import type { NotionSyncStore } from '../store/store.ts' type SqlRow = Record const decodeBodyProjectionPayloadJson = Schema.decodeUnknownSync( - Schema.parseJson(BodyProjectionPayload), + Schema.fromJsonString(BodyProjectionPayload), ) /** Schema version stored in the replica's `PRAGMA user_version`. */ @@ -3747,7 +3747,7 @@ export const readPendingReplicaChanges = (replicaPath: string): readonly Replica ).map((row) => ({ changeId: readString({ row, key: 'change_id' }), kind: Schema.decodeUnknownSync( - Schema.Literal( + Schema.Literals([ 'cell_patch', 'row_archive', 'row_restore', @@ -3758,7 +3758,7 @@ export const readPendingReplicaChanges = (replicaPath: string): readonly Replica 'file_attach', 'view_change', 'conflict_resolution', - ), + ]), )(readString({ row, key: 'kind' })), dataSourceId: readString({ row, key: 'data_source_id' }), pageId: readOptionalString({ row, key: 'page_id' }), @@ -4561,7 +4561,7 @@ export const replicaChangesToPlannerIntents = ({ dataSourceId = decode({ schema: DataSourceId, value: dataSourceIdString }) baseMetadataHash = decode({ schema: Hash, value: change.baseHash }) currentMetadata = Schema.decodeUnknownSync( - Schema.parseJson(CanonicalDataSourceMetadata), + Schema.fromJsonString(CanonicalDataSourceMetadata), )(metadataJson) } catch { markChange({ @@ -4679,9 +4679,9 @@ export const replicaChangesToPlannerIntents = ({ try { dataSourceId = decode({ schema: DataSourceId, value: change.dataSourceId }) baseMetadataHash = decode({ schema: Hash, value: change.baseHash }) - currentMetadata = Schema.decodeUnknownSync(Schema.parseJson(CanonicalDataSourceMetadata))( - metadataJson, - ) + currentMetadata = Schema.decodeUnknownSync( + Schema.fromJsonString(CanonicalDataSourceMetadata), + )(metadataJson) } catch { markChange({ replicaPath, @@ -4972,7 +4972,7 @@ export const replicaChangesToPlannerIntents = ({ } else { try { currentValue = decode({ - schema: Schema.parseJson(CanonicalPropertyValue), + schema: Schema.fromJsonString(CanonicalPropertyValue), value: currentValueJson, }) } catch { @@ -5266,7 +5266,7 @@ export const replicaChangesToPlannerIntents = ({ } let value: CanonicalPropertyValue try { - value = Schema.decodeUnknownSync(Schema.parseJson(CanonicalPropertyValue))( + value = Schema.decodeUnknownSync(Schema.fromJsonString(CanonicalPropertyValue))( change.valueJson, ) } catch { @@ -5314,7 +5314,7 @@ export const replicaChangesToPlannerIntents = ({ } let baseValue: CanonicalPropertyValue try { - baseValue = Schema.decodeUnknownSync(Schema.parseJson(CanonicalPropertyValue))( + baseValue = Schema.decodeUnknownSync(Schema.fromJsonString(CanonicalPropertyValue))( readString({ row: cell, key: 'value_json' }), ) } catch { diff --git a/packages/@overeng/notion-datasource-sync/src/store/projections.ts b/packages/@overeng/notion-datasource-sync/src/store/projections.ts index 42e9ba73d9..b90db74a67 100644 --- a/packages/@overeng/notion-datasource-sync/src/store/projections.ts +++ b/packages/@overeng/notion-datasource-sync/src/store/projections.ts @@ -17,7 +17,7 @@ import { type PropertyAvailability, type PropertyWriteClass } from '../core/guar import { PROJECTOR_VERSION } from './schema.ts' /** All possible lifecycle states of an outbox command row. */ -export const OutboxState = Schema.Literal( +export const OutboxState = Schema.Literals([ 'queued', 'running', 'retryable', @@ -25,7 +25,7 @@ export const OutboxState = Schema.Literal( 'settled', 'fenced', 'ambiguous', -).annotations({ identifier: 'NotionDatasourceSync.OutboxState' }) +]).annotate({ identifier: 'NotionDatasourceSync.OutboxState' }) export type OutboxState = typeof OutboxState.Type /** Minimal event fields needed to compute a deterministic projection digest. */ @@ -66,28 +66,28 @@ export const isCompactionBlockingOutboxState = (state: OutboxState): boolean => state === 'queued' || state === 'running' || state === 'retryable' || state === 'ambiguous' /** Schema-encoded write-class literal stored in `schema_property_projection`. */ -export const ProjectionPropertyWriteClass = Schema.Literal( +export const ProjectionPropertyWriteClass = Schema.Literals([ 'writable', 'computed', 'unsupported', -).annotations({ identifier: 'NotionDatasourceSync.ProjectionPropertyWriteClass' }) +]).annotate({ identifier: 'NotionDatasourceSync.ProjectionPropertyWriteClass' }) /** TypeScript alias for `PropertyWriteClass` — re-exported alongside the Schema literal for symmetry. */ export type ProjectionPropertyWriteClass = PropertyWriteClass /** Schema-encoded availability literal stored in `property_shadow_projection`. */ -export const ProjectionPropertyAvailability = Schema.Literal( +export const ProjectionPropertyAvailability = Schema.Literals([ 'complete', 'computed', 'unsupported', 'paginated-incomplete', 'relation-target-inaccessible', 'related-data-source-unshared', -).annotations({ identifier: 'NotionDatasourceSync.ProjectionPropertyAvailability' }) +]).annotate({ identifier: 'NotionDatasourceSync.ProjectionPropertyAvailability' }) /** TypeScript alias for `PropertyAvailability` — re-exported alongside the Schema literal for symmetry. */ export type ProjectionPropertyAvailability = PropertyAvailability /** Schema-encoded direct-retrieve outcome stored in `query_absence_projection`. */ -export const ProjectionDirectRetrieve = Schema.Literal( +export const ProjectionDirectRetrieve = Schema.Literals([ 'not-run', 'accessible', 'in-trash', @@ -95,7 +95,7 @@ export const ProjectionDirectRetrieve = Schema.Literal( 'permission-ambiguous', 'inaccessible', 'unknown', -).annotations({ identifier: 'NotionDatasourceSync.ProjectionDirectRetrieve' }) +]).annotate({ identifier: 'NotionDatasourceSync.ProjectionDirectRetrieve' }) export type ProjectionDirectRetrieve = typeof ProjectionDirectRetrieve.Type /** Auxiliary JSON payload stored alongside `data_source_projection` rows (schema property list). */ @@ -105,26 +105,28 @@ export const DataSourceProjectionPayload = Schema.Struct({ Schema.Struct({ _tag: Schema.optional(Schema.Literal('DataSourcePropertySnapshot')), propertyId: PropertyId, - name: Schema.optional(Schema.NonEmptyTrimmedString), - type: Schema.optional(Schema.NonEmptyTrimmedString), + name: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), + type: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), configHash: Hash, writeClass: ProjectionPropertyWriteClass, - ordinal: Schema.optional(Schema.NonNegativeInt), + ordinal: Schema.optional(Schema.Natural), configJson: Schema.optional(Schema.String), }), ), ), -}).annotations({ identifier: 'NotionDatasourceSync.DataSourceProjectionPayload' }) +}).annotate({ identifier: 'NotionDatasourceSync.DataSourceProjectionPayload' }) export type DataSourceProjectionPayload = typeof DataSourceProjectionPayload.Type /** Auxiliary JSON payload stored alongside `row_projection` rows (body path, move status, sidecar identity). */ export const RowProjectionPayload = Schema.Struct({ movedOut: Schema.optional(Schema.Boolean), localDeleteCandidate: Schema.optional(Schema.Boolean), - bodyPath: Schema.optional(Schema.NonEmptyTrimmedString), + bodyPath: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), sidecarIdentityProven: Schema.optional(Schema.Boolean), - ownWriteMaterializationIds: Schema.optional(Schema.Array(Schema.NonEmptyTrimmedString)), -}).annotations({ identifier: 'NotionDatasourceSync.RowProjectionPayload' }) + ownWriteMaterializationIds: Schema.optional( + Schema.Array(Schema.Trimmed.check(Schema.isNonEmpty())), + ), +}).annotate({ identifier: 'NotionDatasourceSync.RowProjectionPayload' }) export type RowProjectionPayload = typeof RowProjectionPayload.Type /** Auxiliary JSON payload stored alongside `page_property_checkpoint` rows (base hash, availability). */ @@ -132,14 +134,14 @@ export const PropertyCheckpointProjectionPayload = Schema.Struct({ baseHash: Schema.optional(Hash), availability: Schema.optional(ProjectionPropertyAvailability), valueJson: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.PropertyCheckpointProjectionPayload' }) +}).annotate({ identifier: 'NotionDatasourceSync.PropertyCheckpointProjectionPayload' }) export type PropertyCheckpointProjectionPayload = typeof PropertyCheckpointProjectionPayload.Type /** Auxiliary JSON payload stored alongside `query_scan_checkpoint` rows (cap/contract-change flags). */ export const QueryCheckpointProjectionPayload = Schema.Struct({ cappedAtLimit: Schema.optional(Schema.Boolean), contractChanged: Schema.optional(Schema.Boolean), -}).annotations({ identifier: 'NotionDatasourceSync.QueryCheckpointProjectionPayload' }) +}).annotate({ identifier: 'NotionDatasourceSync.QueryCheckpointProjectionPayload' }) export type QueryCheckpointProjectionPayload = typeof QueryCheckpointProjectionPayload.Type /** Auxiliary JSON payload stored alongside `query_absence_projection` rows (absence classification details). */ @@ -151,15 +153,15 @@ export const QueryAbsenceProjectionPayload = Schema.Struct({ membershipScope: Schema.optional(QueryMembershipScope), filtered: Schema.optional(Schema.Boolean), directRetrieve: Schema.optional(ProjectionDirectRetrieve), -}).annotations({ identifier: 'NotionDatasourceSync.QueryAbsenceProjectionPayload' }) +}).annotate({ identifier: 'NotionDatasourceSync.QueryAbsenceProjectionPayload' }) export type QueryAbsenceProjectionPayload = typeof QueryAbsenceProjectionPayload.Type /** Materialization metadata stored with a body pointer projection. */ export const BodyProjectionMaterialization = Schema.Struct({ path: WorkspaceRelativePath, sidecarIdentityProven: Schema.Boolean, - ownWriteMaterializationIds: Schema.Array(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'NotionDatasourceSync.BodyProjectionMaterialization' }) + ownWriteMaterializationIds: Schema.Array(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'NotionDatasourceSync.BodyProjectionMaterialization' }) export type BodyProjectionMaterialization = typeof BodyProjectionMaterialization.Type /** Auxiliary JSON payload stored alongside `body_pointer_projection` rows. */ @@ -168,5 +170,5 @@ export const BodyProjectionPayload = Schema.TaggedStruct('BodyProjectionPayload' pointer: BodyPointer, safety: BodySafetySnapshot, materialization: BodyProjectionMaterialization, -}).annotations({ identifier: 'NotionDatasourceSync.BodyProjectionPayload' }) +}).annotate({ identifier: 'NotionDatasourceSync.BodyProjectionPayload' }) export type BodyProjectionPayload = typeof BodyProjectionPayload.Type diff --git a/packages/@overeng/notion-datasource-sync/src/store/store.ts b/packages/@overeng/notion-datasource-sync/src/store/store.ts index f845fe7402..cef69b80b9 100644 --- a/packages/@overeng/notion-datasource-sync/src/store/store.ts +++ b/packages/@overeng/notion-datasource-sync/src/store/store.ts @@ -288,7 +288,7 @@ export type WorkspaceBindingRow = { readonly storeIdentity: string } -const decodeEventFromJson = Schema.decodeSync(Schema.parseJson(SyncEvent)) +const decodeEventFromJson = Schema.decodeSync(Schema.fromJsonString(SyncEvent)) const encodeEvent = Schema.encodeSync(SyncEvent) const decodeCapabilityName = Schema.decodeUnknownSync(CapabilityName) const decodeDataSourceId = Schema.decodeSync(DataSourceId) @@ -300,30 +300,32 @@ const decodePropertyId = Schema.decodeSync(PropertyId) const decodeQueryCursor = Schema.decodeSync(QueryCursor) const decodeWorkspaceRelativePath = Schema.decodeUnknownSync(WorkspaceRelativePath) const decodeRemoteWritePlanPayload = Schema.decodeUnknownSync( - Schema.parseJson(RemoteWritePlanPayload), + Schema.fromJsonString(RemoteWritePlanPayload), ) const decodeSurfaceKey = Schema.decodeUnknownSync(SurfaceKey) const decodeSyncEventId = Schema.decodeSync(SyncEventId) const decodeDataSourceProjectionPayload = Schema.decodeUnknownSync( - Schema.parseJson(DataSourceProjectionPayload), + Schema.fromJsonString(DataSourceProjectionPayload), +) +const decodeRowProjectionPayload = Schema.decodeUnknownSync( + Schema.fromJsonString(RowProjectionPayload), ) -const decodeRowProjectionPayload = Schema.decodeUnknownSync(Schema.parseJson(RowProjectionPayload)) const decodeBodyProjectionPayload = Schema.decodeUnknownSync( - Schema.parseJson(BodyProjectionPayload), + Schema.fromJsonString(BodyProjectionPayload), ) const encodeBodyProjectionPayload = Schema.encodeSync(BodyProjectionPayload) const encodeBodyPointer = Schema.encodeSync(BodyPointer) const decodePropertyCheckpointProjectionPayload = Schema.decodeUnknownSync( - Schema.parseJson(PropertyCheckpointProjectionPayload), + Schema.fromJsonString(PropertyCheckpointProjectionPayload), ) const decodeQueryCheckpointProjectionPayload = Schema.decodeUnknownSync( - Schema.parseJson(QueryCheckpointProjectionPayload), + Schema.fromJsonString(QueryCheckpointProjectionPayload), ) const decodeQueryAbsenceProjectionPayload = Schema.decodeUnknownSync( - Schema.parseJson(QueryAbsenceProjectionPayload), + Schema.fromJsonString(QueryAbsenceProjectionPayload), ) const decodeConflictPayloadMessage = Schema.decodeUnknownSync( - Schema.parseJson(Schema.Struct({ message: Schema.optional(Schema.String) })), + Schema.fromJsonString(Schema.Struct({ message: Schema.optional(Schema.String) })), ) const projectionName = 'core' @@ -422,7 +424,7 @@ const readConflictState = ({ readonly key: string }): ConflictProjectionRow['state'] => Schema.decodeUnknownSync( - Schema.Literal('open', 'resolving', 'resolved', 'superseded', 'ignored'), + Schema.Literals(['open', 'resolving', 'resolved', 'superseded', 'ignored']), )(readString({ row, key })) const readTombstoneClassification = ({ @@ -433,14 +435,14 @@ const readTombstoneClassification = ({ readonly key: string }): TombstoneProjectionRow['classification'] => Schema.decodeUnknownSync( - Schema.Literal( + Schema.Literals([ 'unclassified', 'remote_trash', 'moved_out', 'moved_between_tracked_sources', 'inaccessible', 'unknown', - ), + ]), )(readString({ row, key })) const readSignalState = ({ @@ -695,7 +697,7 @@ export class NotionSyncStore { ? undefined : decodeHash(readString({ row: row, key: 'base_hash' })), desiredHash: decodeHash(readString({ row: row, key: 'desired_hash' })), - preflight: Schema.decodeSync(Schema.parseJson(Schema.Array(GuardName)))( + preflight: Schema.decodeSync(Schema.fromJsonString(Schema.Array(GuardName)))( readString({ row: row, key: 'preflight_json' }), ), attempt, @@ -1491,7 +1493,7 @@ export class NotionSyncStore { schemaHash: decodeHash(readString({ row: row, key: 'schema_hash' })), configHash: decodeHash(readString({ row: row, key: 'config_hash' })), writeClass: Schema.decodeUnknownSync( - Schema.Literal('writable', 'computed', 'unsupported'), + Schema.Literals(['writable', 'computed', 'unsupported']), )(readString({ row: row, key: 'write_class' })), })), rows: this.#db @@ -1528,14 +1530,14 @@ export class NotionSyncStore { baseHash: decodeHash(readString({ row: row, key: 'base_hash' })), remoteHash: decodeHash(readString({ row: row, key: 'remote_hash' })), availability: Schema.decodeUnknownSync( - Schema.Literal( + Schema.Literals([ 'complete', 'computed', 'unsupported', 'paginated-incomplete', 'relation-target-inaccessible', 'related-data-source-unshared', - ), + ]), )(readString({ row: row, key: 'availability' })), pendingLocal: pendingProperties.get(`${pageId}\0${propertyId}`), } @@ -1567,7 +1569,7 @@ export class NotionSyncStore { pointer: payload.pointer, sidecarIdentityProven: readBoolean({ row: row, key: 'sidecar_identity_proven' }), ownWriteMaterializationIds: Schema.decodeSync( - Schema.parseJson(Schema.Array(Schema.String)), + Schema.fromJsonString(Schema.Array(Schema.String)), )(readString({ row: row, key: 'own_write_materialization_ids_json' })), safety: payload.safety, } @@ -2289,7 +2291,7 @@ CREATE TABLE _nds_conflict ( ? classification : 'not-run' : Schema.decodeUnknownSync( - Schema.Literal( + Schema.Literals([ 'not-run', 'accessible', 'in-trash', @@ -2297,7 +2299,7 @@ CREATE TABLE _nds_conflict ( 'permission-ambiguous', 'inaccessible', 'unknown', - ), + ]), )(readString({ row: row, key: 'direct_retrieve' })) return { @@ -2360,11 +2362,11 @@ CREATE TABLE _nds_conflict ( absence: { classified: readBoolean({ row: row, key: 'classified' }), membershipScope: Schema.decodeUnknownSync( - Schema.Literal('all-data-source-rows', 'explicit-filter'), + Schema.Literals(['all-data-source-rows', 'explicit-filter']), )(readString({ row: row, key: 'membership_scope' })), filtered: readBoolean({ row: row, key: 'filtered' }), directRetrieve: Schema.decodeUnknownSync( - Schema.Literal( + Schema.Literals([ 'not-run', 'accessible', 'in-trash', @@ -2372,7 +2374,7 @@ CREATE TABLE _nds_conflict ( 'permission-ambiguous', 'inaccessible', 'unknown', - ), + ]), )(readString({ row: row, key: 'direct_retrieve' })), }, })) diff --git a/packages/@overeng/notion-datasource-sync/src/sync/executor.ts b/packages/@overeng/notion-datasource-sync/src/sync/executor.ts index 4ae1919d94..f9b069fad0 100644 --- a/packages/@overeng/notion-datasource-sync/src/sync/executor.ts +++ b/packages/@overeng/notion-datasource-sync/src/sync/executor.ts @@ -74,10 +74,10 @@ type RemoteWriteResult = { /** Canonical payload hashed to verify a relation property patch — the sorted target page ids under a stable tag. */ const RelationVerificationPayload = Schema.TaggedStruct('relation', { pageIds: Schema.Array(Schema.String), -}).annotations({ identifier: 'NotionDatasourceSync.RelationVerificationPayload' }) +}).annotate({ identifier: 'NotionDatasourceSync.RelationVerificationPayload' }) const encodeRelationVerificationJson = Schema.encodeSync( - Schema.parseJson(RelationVerificationPayload), + Schema.fromJsonString(RelationVerificationPayload), ) const relationPatchVerificationHash = ( diff --git a/packages/@overeng/notion-datasource-sync/src/testing/live-notion.ts b/packages/@overeng/notion-datasource-sync/src/testing/live-notion.ts index 6c0efb17a4..6e9293bba6 100644 --- a/packages/@overeng/notion-datasource-sync/src/testing/live-notion.ts +++ b/packages/@overeng/notion-datasource-sync/src/testing/live-notion.ts @@ -3,8 +3,8 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' -import { FetchHttpClient, HttpClient } from '@effect/platform' import { Chunk, Effect, Layer, Redacted, Schema, Stream } from 'effect' +import { FetchHttpClient, HttpClient } from 'effect/unstable/http' import { NOTION_API_VERSION, diff --git a/packages/@overeng/notion-datasource-sync/src/webhook/notion.ts b/packages/@overeng/notion-datasource-sync/src/webhook/notion.ts index 0b69133f19..e21fc770b6 100644 --- a/packages/@overeng/notion-datasource-sync/src/webhook/notion.ts +++ b/packages/@overeng/notion-datasource-sync/src/webhook/notion.ts @@ -79,7 +79,7 @@ const rawBodyText = (rawBody: string | Uint8Array): string => typeof rawBody === 'string' ? rawBody : textDecoder.decode(rawBody) /** Decode a raw body string into a JSON value, returning Either. */ -const decodeJson = Schema.decodeUnknownEither(Schema.parseJson()) +const decodeJson = Schema.decodeUnknownEither(Schema.fromJsonString(Schema.Unknown)) /** Decode a JSON value into `NotionWebhookPayload`, returning Either. */ const decodePayload = Schema.decodeUnknownEither( @@ -95,8 +95,8 @@ const NotionWebhookVerificationStruct = Schema.Struct({ // NonEmptyTrimmedString trims then checks: a whitespace-only token (e.g. " ") // intentionally fails this decode and falls through to the HMAC gate rather than // being treated as a (meaningless) verification challenge. - verification_token: Schema.NonEmptyTrimmedString, -}).annotations({ identifier: 'NotionWebhook.VerificationStruct' }) + verification_token: Schema.Trimmed.check(Schema.isNonEmpty()), +}).annotate({ identifier: 'NotionWebhook.VerificationStruct' }) const decodeVerification = Schema.decodeUnknownEither(NotionWebhookVerificationStruct, { onExcessProperty: 'preserve', diff --git a/packages/@overeng/notion-effect-client/package.json b/packages/@overeng/notion-effect-client/package.json index 0efdeded6b..bd83fce8d7 100644 --- a/packages/@overeng/notion-effect-client/package.json +++ b/packages/@overeng/notion-effect-client/package.json @@ -14,21 +14,16 @@ } }, "dependencies": { - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@overeng/content-address": "workspace:^", "@overeng/notion-core": "workspace:^", "@overeng/notion-effect-schema": "workspace:^", "@overeng/otel-contract": "workspace:^", "@overeng/utils": "workspace:^", "@playwright/test": "1.61.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "mdast-util-gfm-strikethrough": "2.0.0", "mdast-util-gfm-table": "2.0.0", "mdast-util-gfm-task-list-item": "2.0.0", @@ -42,11 +37,10 @@ "vitest": "4.1.9" }, "devDependencies": { - "@effect/platform": "0.96.2", - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, diff --git a/packages/@overeng/notion-effect-client/package.json.genie.ts b/packages/@overeng/notion-effect-client/package.json.genie.ts index a8e5986e9a..b545164265 100644 --- a/packages/@overeng/notion-effect-client/package.json.genie.ts +++ b/packages/@overeng/notion-effect-client/package.json.genie.ts @@ -20,30 +20,12 @@ const runtimeDeps = catalog.compose({ // must be a real runtime dependency — not a dev/peer dep that a standalone // consumer could fail to provide. workspace: [contentAddressPkg, notionCorePkg, notionEffectSchemaPkg, otelContractPkg, utilsPkg], - external: catalog.pick( - 'mdast-util-gfm-strikethrough', - 'mdast-util-gfm-table', - 'mdast-util-gfm-task-list-item', - 'micromark-extension-gfm-strikethrough', - 'micromark-extension-gfm-table', - 'micromark-extension-gfm-task-list-item', - 'remark-parse', - 'remark-stringify', - 'unified', - 'unist-util-visit', - ), + external: catalog.pick('mdast-util-gfm-strikethrough', 'mdast-util-gfm-table', 'mdast-util-gfm-task-list-item', 'micromark-extension-gfm-strikethrough', 'micromark-extension-gfm-table', 'micromark-extension-gfm-task-list-item', 'remark-parse', 'remark-stringify', 'unified', 'unist-util-visit'), }, devDependencies: { workspace: [utilsDevPkg], external: { - ...catalog.pick( - '@effect/platform', - '@effect/vitest', - '@types/node', - 'effect', - 'typescript', - 'vitest', - ), + ...catalog.pick('effect', '@effect/vitest', '@types/node', 'typescript', 'vitest'), }, }, // `@overeng/utils` is a runtime workspace dep that carries peer dependencies diff --git a/packages/@overeng/notion-effect-client/src/blocks.ts b/packages/@overeng/notion-effect-client/src/blocks.ts index ffe4f5e31f..16a224fbb6 100644 --- a/packages/@overeng/notion-effect-client/src/blocks.ts +++ b/packages/@overeng/notion-effect-client/src/blocks.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Chunk, Effect, Option, Schema, Stream } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { type Block, BlockSchema, type BlockType } from '@overeng/notion-effect-schema' diff --git a/packages/@overeng/notion-effect-client/src/body-evidence.ts b/packages/@overeng/notion-effect-client/src/body-evidence.ts index efc0f4c93e..ce787364f5 100644 --- a/packages/@overeng/notion-effect-client/src/body-evidence.ts +++ b/packages/@overeng/notion-effect-client/src/body-evidence.ts @@ -10,24 +10,24 @@ import { import { NOTION_API_VERSION } from './config.ts' -const BodyCompletenessEvidence = Schema.Literal('complete', 'lossy').annotations({ +const BodyCompletenessEvidence = Schema.Literals(['complete', 'lossy']).annotate({ identifier: 'NotionBodyEvidence.BodyCompleteness', }) type BodyCompletenessEvidence = typeof BodyCompletenessEvidence.Type /** `sha256:` branded fingerprint of a body observation's identity evidence (excludes `observedAt`, so re-observing unchanged content yields the same value). */ -export const BodyEvidenceFingerprint = Schema.String.pipe( - Schema.pattern(/^sha256:[a-f0-9]{64}$/), +export const BodyEvidenceFingerprint = Schema.String.check( + Schema.check(Schema.isPattern(/^sha256:[a-f0-9]{64}$/)), Schema.brand('NotionBodyEvidence.BodyEvidenceFingerprint'), - Schema.annotations({ identifier: 'NotionBodyEvidence.BodyEvidenceFingerprint' }), + Schema.annotate({ identifier: 'NotionBodyEvidence.BodyEvidenceFingerprint' }), ) export type BodyEvidenceFingerprint = typeof BodyEvidenceFingerprint.Type /** Content-addressed evidence for one remote page body observation: endpoint markdown, block tree, rendered body, and inventory as content descriptors plus the stability window. */ export const RemoteBodyObservationEvidence = Schema.TaggedStruct('RemoteBodyObservationEvidence', { schemaVersion: Schema.Literal(1), - notionApiVersion: Schema.NonEmptyTrimmedString, - pageId: Schema.NonEmptyTrimmedString, + notionApiVersion: Schema.Trimmed.check(Schema.isNonEmpty()), + pageId: Schema.Trimmed.check(Schema.isNonEmpty()), observedAt: Schema.DateTimeUtc, observationWindow: Schema.Struct({ beforeLastEditedTime: Schema.DateTimeUtc, @@ -38,13 +38,13 @@ export const RemoteBodyObservationEvidence = Schema.TaggedStruct('RemoteBodyObse renderedBody: ContentDescriptor, blockInventory: ContentDescriptor, completeness: BodyCompletenessEvidence, -}).annotations({ identifier: 'NotionBodyEvidence.RemoteBodyObservationEvidence' }) +}).annotate({ identifier: 'NotionBodyEvidence.RemoteBodyObservationEvidence' }) export type RemoteBodyObservationEvidence = typeof RemoteBodyObservationEvidence.Type const RemoteBodyObservationIdentityEvidence = Schema.TaggedStruct('RemoteBodyObservationEvidence', { schemaVersion: Schema.Literal(1), - notionApiVersion: Schema.NonEmptyTrimmedString, - pageId: Schema.NonEmptyTrimmedString, + notionApiVersion: Schema.Trimmed.check(Schema.isNonEmpty()), + pageId: Schema.Trimmed.check(Schema.isNonEmpty()), observationWindow: Schema.Struct({ beforeLastEditedTime: Schema.DateTimeUtc, afterLastEditedTime: Schema.DateTimeUtc, @@ -54,31 +54,31 @@ const RemoteBodyObservationIdentityEvidence = Schema.TaggedStruct('RemoteBodyObs renderedBody: ContentDescriptor, blockInventory: ContentDescriptor, completeness: BodyCompletenessEvidence, -}).annotations({ identifier: 'NotionBodyEvidence.RemoteBodyObservationIdentityEvidence' }) +}).annotate({ identifier: 'NotionBodyEvidence.RemoteBodyObservationIdentityEvidence' }) const BlockInventoryEntryEvidence = Schema.Struct({ id: Schema.String, type: Schema.String, hasChildren: Schema.Boolean, inTrash: Schema.Boolean, -}).annotations({ identifier: 'NotionBodyEvidence.BlockInventoryEntry' }) +}).annotate({ identifier: 'NotionBodyEvidence.BlockInventoryEntry' }) const BlockTreeEntryEvidence = Schema.Struct({ - depth: Schema.NonNegativeInt, + depth: Schema.Natural, id: Schema.String, type: Schema.String, hasChildren: Schema.Boolean, inTrash: Schema.Boolean, -}).annotations({ identifier: 'NotionBodyEvidence.BlockTreeEntry' }) +}).annotate({ identifier: 'NotionBodyEvidence.BlockTreeEntry' }) const BlockInventoryEvidence = Schema.Struct({ entries: Schema.Array(BlockInventoryEntryEvidence), renderedMarkdown: Schema.String, -}).annotations({ identifier: 'NotionBodyEvidence.BlockInventory' }) +}).annotate({ identifier: 'NotionBodyEvidence.BlockInventory' }) const BlockTreeEvidence = Schema.Struct({ entries: Schema.Array(BlockTreeEntryEvidence), -}).annotations({ identifier: 'NotionBodyEvidence.BlockTree' }) +}).annotate({ identifier: 'NotionBodyEvidence.BlockTree' }) type BodyEvidenceBlockTree = ReadonlyArray<{ readonly block: { diff --git a/packages/@overeng/notion-effect-client/src/body-observation.ts b/packages/@overeng/notion-effect-client/src/body-observation.ts index 0e30ebf189..3ba2220dc4 100644 --- a/packages/@overeng/notion-effect-client/src/body-observation.ts +++ b/packages/@overeng/notion-effect-client/src/body-observation.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect, Schema } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { classifyBodyCompleteness, @@ -37,7 +37,7 @@ export interface NotionBodyObservation { } /** Raised when a live body observation cannot find a stable page metadata window. */ -export class NotionBodyObservationChangedError extends Schema.TaggedError()( +export class NotionBodyObservationChangedError extends Schema.TaggedErrorClass()( 'NotionBodyObservationChangedError', { pageId: Schema.String, diff --git a/packages/@overeng/notion-effect-client/src/body-observation.unit.test.ts b/packages/@overeng/notion-effect-client/src/body-observation.unit.test.ts index 667c015e77..d5dd966084 100644 --- a/packages/@overeng/notion-effect-client/src/body-observation.unit.test.ts +++ b/packages/@overeng/notion-effect-client/src/body-observation.unit.test.ts @@ -1,5 +1,5 @@ -import type { HttpClientRequest } from '@effect/platform' import { Effect, Either } from 'effect' +import type { HttpClientRequest } from 'effect/unstable/http' import { describe, expect, it } from 'vitest' import type { Block } from '@overeng/notion-effect-schema' diff --git a/packages/@overeng/notion-effect-client/src/comments.ts b/packages/@overeng/notion-effect-client/src/comments.ts index 2989261d2c..cff53c192f 100644 --- a/packages/@overeng/notion-effect-client/src/comments.ts +++ b/packages/@overeng/notion-effect-client/src/comments.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Chunk, Effect, Option, Stream } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { type Comment, CommentSchema } from '@overeng/notion-effect-schema' diff --git a/packages/@overeng/notion-effect-client/src/config.ts b/packages/@overeng/notion-effect-client/src/config.ts index 34f9a6f717..8cc7862d20 100644 --- a/packages/@overeng/notion-effect-client/src/config.ts +++ b/packages/@overeng/notion-effect-client/src/config.ts @@ -17,7 +17,9 @@ export interface NotionClientConfig { } /** Context tag for NotionClientConfig */ -export class NotionConfig extends Context.Tag('NotionConfig')() {} +export class NotionConfig extends Context.Service()( + 'NotionConfig', +) {} /** * Environment variable names checked for the Notion API token, in precedence order. @@ -27,7 +29,7 @@ export class NotionConfig extends Context.Tag('NotionConfig')()( +export class NotionTokenMissing extends Schema.TaggedErrorClass()( 'NotionTokenMissing', { message: Schema.String, diff --git a/packages/@overeng/notion-effect-client/src/custom-emojis.ts b/packages/@overeng/notion-effect-client/src/custom-emojis.ts index c7fe83804c..8173f9a76b 100644 --- a/packages/@overeng/notion-effect-client/src/custom-emojis.ts +++ b/packages/@overeng/notion-effect-client/src/custom-emojis.ts @@ -10,14 +10,14 @@ const CustomEmojiSchema = Schema.Struct({ id: Schema.String, name: Schema.String, url: Schema.String, -}).annotations({ identifier: 'CustomEmoji' }) +}).annotate({ identifier: 'CustomEmoji' }) export type CustomEmoji = typeof CustomEmojiSchema.Type const CustomEmojisResponseSchema = Schema.Struct({ object: Schema.Literal('list'), results: Schema.Array(CustomEmojiSchema), -}).annotations({ identifier: 'CustomEmojisResponse' }) +}).annotate({ identifier: 'CustomEmojisResponse' }) // ----------------------------------------------------------------------------- // Service Implementation diff --git a/packages/@overeng/notion-effect-client/src/databases.ts b/packages/@overeng/notion-effect-client/src/databases.ts index 4f7bd82706..12df92ebe0 100644 --- a/packages/@overeng/notion-effect-client/src/databases.ts +++ b/packages/@overeng/notion-effect-client/src/databases.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect, Option, type Schema, Stream } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import type { DataSourceSchema, Page } from '@overeng/notion-effect-schema' import { diff --git a/packages/@overeng/notion-effect-client/src/databases.unit.test.ts b/packages/@overeng/notion-effect-client/src/databases.unit.test.ts index c6b91314b2..09a8c9be0e 100644 --- a/packages/@overeng/notion-effect-client/src/databases.unit.test.ts +++ b/packages/@overeng/notion-effect-client/src/databases.unit.test.ts @@ -1,5 +1,5 @@ -import type { HttpClientRequest } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClientRequest } from 'effect/unstable/http' import { expect } from 'vitest' import { Vitest } from '@overeng/utils-dev/node-vitest' diff --git a/packages/@overeng/notion-effect-client/src/error.ts b/packages/@overeng/notion-effect-client/src/error.ts index 2de0d4cc2e..bbc34759dc 100644 --- a/packages/@overeng/notion-effect-client/src/error.ts +++ b/packages/@overeng/notion-effect-client/src/error.ts @@ -5,7 +5,7 @@ import { Option, Schema } from 'effect' * * @see https://developers.notion.com/reference/errors */ -export const NotionErrorCode = Schema.Literal( +export const NotionErrorCode = Schema.Literals([ 'invalid_json', 'invalid_request_url', 'invalid_request', @@ -20,7 +20,7 @@ export const NotionErrorCode = Schema.Literal( 'service_unavailable', 'database_connection_unavailable', 'gateway_timeout', -) +]) export type NotionErrorCode = typeof NotionErrorCode.Type @@ -32,7 +32,7 @@ export type NotionErrorCode = typeof NotionErrorCode.Type * * @see https://developers.notion.com/reference/errors */ -export class NotionApiError extends Schema.TaggedError()('NotionApiError', { +export class NotionApiError extends Schema.TaggedErrorClass()('NotionApiError', { /** HTTP status code */ status: Schema.Number, /** Notion-specific error code */ @@ -42,11 +42,11 @@ export class NotionApiError extends Schema.TaggedError()('Notion /** Retry delay in seconds (from retry-after header, typically for rate limits) */ retryAfterSeconds: Schema.OptionFromSelf(Schema.Number), /** Request ID for Notion support (from x-request-id header) */ - requestId: Schema.optionalWith(Schema.String, { as: 'Option' }), + requestId: Schema.OptionFromOptional(Schema.String), /** Original request URL for debugging */ - url: Schema.optionalWith(Schema.String, { as: 'Option' }), + url: Schema.OptionFromOptional(Schema.String), /** Original request method for debugging */ - method: Schema.optionalWith(Schema.String, { as: 'Option' }), + method: Schema.OptionFromOptional(Schema.String), }) { /** Check if error is retryable (rate limit or server error) */ get isRetryable(): boolean { diff --git a/packages/@overeng/notion-effect-client/src/files.ts b/packages/@overeng/notion-effect-client/src/files.ts index 7ee717ac42..6e24d1ebf8 100644 --- a/packages/@overeng/notion-effect-client/src/files.ts +++ b/packages/@overeng/notion-effect-client/src/files.ts @@ -28,7 +28,7 @@ const FileUploadResponseSchema = Schema.Struct({ }) /** Request body for creating a single-part file upload. */ -const CreateFileUploadRequestJson = Schema.parseJson( +const CreateFileUploadRequestJson = Schema.fromJsonString( Schema.Struct({ mode: Schema.Literal('single_part'), filename: Schema.String, diff --git a/packages/@overeng/notion-effect-client/src/internal/http.ts b/packages/@overeng/notion-effect-client/src/internal/http.ts index 3a8be672c5..334026723a 100644 --- a/packages/@overeng/notion-effect-client/src/internal/http.ts +++ b/packages/@overeng/notion-effect-client/src/internal/http.ts @@ -1,9 +1,3 @@ -import { - HttpClient, - type HttpClientError, - HttpClientRequest, - type HttpClientResponse, -} from '@effect/platform' import { Context, Effect, @@ -14,6 +8,12 @@ import { ScheduleInterval, Schema, } from 'effect' +import { + HttpClient, + type HttpClientError, + HttpClientRequest, + type HttpClientResponse, +} from 'effect/unstable/http' import { NOTION_API_BASE_URL, NOTION_API_VERSION, NotionConfig } from '../config.ts' import { NotionApiError, NotionErrorResponse } from '../error.ts' @@ -66,9 +66,10 @@ export type NotionHttpTelemetryReporter = { } /** Optional Effect service used by callers that want realtime HTTP/rate-limit visibility. */ -export class NotionHttpTelemetry extends Context.Tag( - '@overeng/notion-effect-client/NotionHttpTelemetry', -)() {} +export class NotionHttpTelemetry extends Context.Service< + NotionHttpTelemetry, + NotionHttpTelemetryReporter +>()('@overeng/notion-effect-client/NotionHttpTelemetry') {} /** Options for building a Notion API request */ export interface BuildRequestOptions { diff --git a/packages/@overeng/notion-effect-client/src/internal/http.unit.test.ts b/packages/@overeng/notion-effect-client/src/internal/http.unit.test.ts index 86ef56e426..1a7c1fa1d6 100644 --- a/packages/@overeng/notion-effect-client/src/internal/http.unit.test.ts +++ b/packages/@overeng/notion-effect-client/src/internal/http.unit.test.ts @@ -1,5 +1,5 @@ -import type { HttpClientRequest } from '@effect/platform' import { Effect, Fiber, Option, Redacted, Schema, TestClock, Tracer } from 'effect' +import type { HttpClientRequest } from 'effect/unstable/http' import { expect } from 'vitest' import { Vitest } from '@overeng/utils-dev/node-vitest' diff --git a/packages/@overeng/notion-effect-client/src/internal/pagination.unit.test.ts b/packages/@overeng/notion-effect-client/src/internal/pagination.unit.test.ts index afb2149e2d..3890f6cc64 100644 --- a/packages/@overeng/notion-effect-client/src/internal/pagination.unit.test.ts +++ b/packages/@overeng/notion-effect-client/src/internal/pagination.unit.test.ts @@ -11,7 +11,7 @@ import { toPaginatedResult, } from './pagination.ts' -class PaginationNetworkError extends Schema.TaggedError()( +class PaginationNetworkError extends Schema.TaggedErrorClass()( 'PaginationNetworkError', { message: Schema.String, diff --git a/packages/@overeng/notion-effect-client/src/internal/rate-limit-signals.e2e.test.ts b/packages/@overeng/notion-effect-client/src/internal/rate-limit-signals.e2e.test.ts index e81b9ed893..f5d93be571 100644 --- a/packages/@overeng/notion-effect-client/src/internal/rate-limit-signals.e2e.test.ts +++ b/packages/@overeng/notion-effect-client/src/internal/rate-limit-signals.e2e.test.ts @@ -1,3 +1,4 @@ +import { Effect, Fiber, Metric, Redacted, Schema, TestClock, Tracer } from 'effect' /** * Deterministic e2e coverage for the rate-limit PRESSURE signals (decision 0017 * Half 2, #775). The fake gateway in `@overeng/notion-datasource-sync` never @@ -23,8 +24,7 @@ * This file is `*.e2e.test.ts`, exempt from `overeng/no-raw-otel-primitives`, so it * may read raw `Metric` values to assert the OtelMetric-emitted counters moved. */ -import { HttpClient } from '@effect/platform' -import { Effect, Fiber, Metric, Redacted, Schema, TestClock, Tracer } from 'effect' +import { HttpClient } from 'effect/unstable/http' import { expect } from 'vitest' import { Vitest } from '@overeng/utils-dev/node-vitest' diff --git a/packages/@overeng/notion-effect-client/src/internal/throttle.ts b/packages/@overeng/notion-effect-client/src/internal/throttle.ts index 7c7637c230..a9e514d928 100644 --- a/packages/@overeng/notion-effect-client/src/internal/throttle.ts +++ b/packages/@overeng/notion-effect-client/src/internal/throttle.ts @@ -21,10 +21,10 @@ export interface NotionThrottleOptions { * `apply` wraps one logical request (the whole retry loop) so a single token is * consumed per request, not per retry attempt. */ -export class NotionThrottle extends Context.Tag('@overeng/notion-effect-client/NotionThrottle')< +export class NotionThrottle extends Context.Service< NotionThrottle, { readonly apply: (effect: Effect.Effect) => Effect.Effect } ->() {} +>()('@overeng/notion-effect-client/NotionThrottle') {} /** * Build a scoped {@link NotionThrottle} layer from an Effect-native diff --git a/packages/@overeng/notion-effect-client/src/nmd.ts b/packages/@overeng/notion-effect-client/src/nmd.ts index fd01390336..089a48396a 100644 --- a/packages/@overeng/notion-effect-client/src/nmd.ts +++ b/packages/@overeng/notion-effect-client/src/nmd.ts @@ -10,9 +10,9 @@ import { import { NOTION_API_VERSION } from './config.ts' /** SHA-256 digest string used for canonical body and file content identity. */ -export const Sha256Digest = Schema.String.pipe( - Schema.pattern(/^sha256:[a-f0-9]{64}$/i), -).annotations({ +export const Sha256Digest = Schema.String.check( + Schema.check(Schema.isPattern(/^sha256:[a-f0-9]{64}$/i)), +).annotate({ identifier: 'NotionMd.Sha256Digest', }) @@ -30,7 +30,7 @@ export const RelativePath = Schema.String.pipe( message: () => 'Expected a non-empty relative path without parent traversal', }, ), -).annotations({ +).annotate({ identifier: 'NotionMd.RelativePath', }) @@ -71,12 +71,12 @@ export const nmdSyncStateRelativePath = (pageId: string): RelativePath => decodeRelativePath(`${NMD_SYNC_DIRECTORY}/${pageId}.json`) /** Role of a content-addressed local object referenced by `.nmd` frontmatter. */ -export const NmdObjectRole = Schema.Literal( +export const NmdObjectRole = Schema.Literals([ 'base_snapshot', 'storage_payload', 'file_payload', 'comment_payload', -).annotations({ +]).annotate({ identifier: 'NotionMd.ObjectRole', }) @@ -88,8 +88,8 @@ export const NmdObjectRef = Schema.TaggedStruct('object_ref', { hash: Sha256Digest, path: RelativePath, media_type: Schema.String, - byte_length: Schema.NonNegativeInt, -}).annotations({ + byte_length: Schema.Natural, +}).annotate({ identifier: 'NotionMd.ObjectRef', }) @@ -126,14 +126,14 @@ export const makeNmdObjectRef = (opts: { * - `shared` — bidirectional. The ONLY value that engages the base snapshot + * 3-way merge apparatus (R32); must carry a `page_id`. */ -export const NmdSource = Schema.Literal('local', 'remote', 'shared').annotations({ +export const NmdSource = Schema.Literals(['local', 'remote', 'shared']).annotate({ identifier: 'NotionMd.Source', }) export type NmdSource = typeof NmdSource.Type /** Parent location of a synced Notion page. */ -export const NmdParentRef = Schema.Union( +export const NmdParentRef = Schema.Union([ Schema.TaggedStruct('page', { id: NotionUUID, }), @@ -154,7 +154,7 @@ export const NmdParentRef = Schema.Union( Schema.TaggedStruct('unknown', { raw: Schema.Unknown, }), -).annotations({ +]).annotate({ identifier: 'NotionMd.ParentRef', }) @@ -169,14 +169,14 @@ export const NmdBodyState = Schema.Struct({ remote_last_edited_time: ISO8601DateTime, truncated: Schema.Boolean, unknown_block_ids: Schema.Array(NotionUUID), -}).annotations({ +}).annotate({ identifier: 'NotionMd.BodyState', }) export type NmdBodyState = typeof NmdBodyState.Type /** Page icon state preserved outside the Markdown body. */ -export const NmdPageIcon = Schema.NullOr(NotionIcon).annotations({ +export const NmdPageIcon = Schema.NullOr(NotionIcon).annotate({ identifier: 'NotionMd.PageIcon', }) @@ -188,7 +188,7 @@ export const NmdExternalFile = Schema.Struct({ external: Schema.Struct({ url: Schema.String, }), -}).annotations({ +}).annotate({ identifier: 'NotionMd.ExternalFile', }) @@ -201,18 +201,16 @@ export const NmdNotionFile = Schema.Struct({ url: Schema.String, expiry_time: ISO8601DateTime, }), -}).annotations({ +}).annotate({ identifier: 'NotionMd.NotionFile', }) export type NmdNotionFile = typeof NmdNotionFile.Type /** Page cover state preserved outside the Markdown body. */ -export const NmdPageCover = Schema.NullOr(Schema.Union(NmdExternalFile, NmdNotionFile)).annotations( - { - identifier: 'NotionMd.PageCover', - }, -) +export const NmdPageCover = Schema.NullOr(Schema.Union([NmdExternalFile, NmdNotionFile])).annotate({ + identifier: 'NotionMd.PageCover', +}) export type NmdPageCover = typeof NmdPageCover.Type @@ -223,7 +221,7 @@ export const NmdPageState = Schema.Struct({ cover: NmdPageCover, in_trash: Schema.Boolean, is_locked: Schema.Boolean, -}).annotations({ +}).annotate({ identifier: 'NotionMd.PageState', }) @@ -234,14 +232,14 @@ export const NmdDateValue = Schema.Struct({ start: Schema.String, end: Schema.NullOr(Schema.String), time_zone: Schema.NullOr(Schema.String), -}).annotations({ +}).annotate({ identifier: 'NotionMd.DateValue', }) export type NmdDateValue = typeof NmdDateValue.Type /** File reference used by typed file properties. */ -export const NmdPropertyFileRef = Schema.Union( +export const NmdPropertyFileRef = Schema.Union([ Schema.TaggedStruct('local_file', { path: RelativePath, content_hash: Schema.optional(Sha256Digest), @@ -256,7 +254,7 @@ export const NmdPropertyFileRef = Schema.Union( Schema.TaggedStruct('external_url', { url: Schema.String, }), -).annotations({ +]).annotate({ identifier: 'NotionMd.PropertyFileRef', }) @@ -270,14 +268,14 @@ export const NmdPlaceValue = Schema.Struct({ address: Schema.optional(Schema.NullOr(Schema.String)), google_place_id: Schema.optional(Schema.NullOr(Schema.String)), aws_place_id: Schema.optional(Schema.NullOr(Schema.String)), -}).annotations({ +}).annotate({ identifier: 'NotionMd.PlaceValue', }) export type NmdPlaceValue = typeof NmdPlaceValue.Type /** Verification value used by typed page-property frontmatter. */ -export const NmdVerificationValue = Schema.Union( +export const NmdVerificationValue = Schema.Union([ Schema.Struct({ state: Schema.Literal('verified'), date: Schema.optional(NmdDateValue), @@ -285,14 +283,14 @@ export const NmdVerificationValue = Schema.Union( Schema.Struct({ state: Schema.Literal('unverified'), }), -).annotations({ +]).annotate({ identifier: 'NotionMd.VerificationValue', }) export type NmdVerificationValue = typeof NmdVerificationValue.Type /** Typed, human-editable page-property value stored in frontmatter. */ -export const NmdPropertyValue = Schema.Union( +export const NmdPropertyValue = Schema.Union([ Schema.TaggedStruct('title', { value: Schema.String }), Schema.TaggedStruct('rich_text', { value: Schema.NullOr(Schema.String) }), Schema.TaggedStruct('number', { value: Schema.NullOr(Schema.Number) }), @@ -313,7 +311,7 @@ export const NmdPropertyValue = Schema.Union( property_type: Schema.String, value: Schema.Unknown, }), -).annotations({ +]).annotate({ identifier: 'NotionMd.PropertyValue', }) @@ -325,9 +323,9 @@ export const NmdDataSourceBinding = Schema.Struct({ data_source_id: NotionUUID, schema_hash: Sha256Digest, title_property: Schema.String, - property_ids: Schema.Record({ key: Schema.String, value: Schema.String }), + property_ids: Schema.Record(Schema.String, Schema.String), read_only_properties: Schema.Array(Schema.String), -}).annotations({ +}).annotate({ identifier: 'NotionMd.DataSourceBinding', }) @@ -349,7 +347,7 @@ export const NmdUnsupportedBlockUnit = Schema.TaggedStruct('unsupported_block', last_edited_time: ISO8601DateTime, payload: Schema.Unknown, }), -}).annotations({ +}).annotate({ identifier: 'NotionMd.UnsupportedBlockUnit', }) @@ -358,7 +356,7 @@ export type NmdUnsupportedBlockUnit = typeof NmdUnsupportedBlockUnit.Type /** File/upload lifecycle unit small enough to keep in frontmatter. */ export const NmdFileUnit = Schema.TaggedStruct('file_unit', { id: Schema.String, - role: Schema.Literal('property_file', 'block_file', 'block_image', 'upload'), + role: Schema.Literals(['property_file', 'block_file', 'block_image', 'upload']), filename: Schema.String, content_type: Schema.optional(Schema.String), content_length: Schema.optional(Schema.Number), @@ -367,7 +365,7 @@ export const NmdFileUnit = Schema.TaggedStruct('file_unit', { block_id: Schema.optional(NotionUUID), file_upload_id: Schema.optional(NotionUUID), status: Schema.optional(Schema.String), -}).annotations({ +}).annotate({ identifier: 'NotionMd.FileUnit', }) @@ -380,14 +378,14 @@ export const NmdCommentUnit = Schema.TaggedStruct('comment_unit', { notion_comment_id: Schema.optional(NotionUUID), notion_discussion_id: Schema.optional(NotionUUID), anchor_text: Schema.optional(Schema.String), -}).annotations({ +}).annotate({ identifier: 'NotionMd.CommentUnit', }) export type NmdCommentUnit = typeof NmdCommentUnit.Type /** Storage strategy declared by a local `.nmd` file. */ -export const NmdStorage = Schema.Union( +export const NmdStorage = Schema.Union([ Schema.TaggedStruct('self_contained', { unsupported_blocks: Schema.Array(NmdUnsupportedBlockUnit), files: Schema.Array(NmdFileUnit), @@ -399,7 +397,7 @@ export const NmdStorage = Schema.Union( file_ids: Schema.Array(Schema.String), comment_ids: Schema.Array(Schema.String), }), -).annotations({ +]).annotate({ identifier: 'NotionMd.Storage', }) @@ -417,10 +415,10 @@ export const NmdFrontmatterV1 = Schema.Struct({ body: NmdBodyState, page: NmdPageState, data_source: Schema.NullOr(NmdDataSourceBinding), - properties: Schema.Record({ key: Schema.String, value: NmdPropertyValue }), + properties: Schema.Record(Schema.String, NmdPropertyValue), storage: NmdStorage, }), -}).annotations({ +}).annotate({ identifier: 'NotionMd.FrontmatterV1', }) @@ -431,7 +429,7 @@ export type NmdFrontmatterV1 = typeof NmdFrontmatterV1.Type * echoes move to the sidecar sync state, so the frontmatter only carries * the property tags a user can actually edit. */ -export const NmdWritablePropertyValue = Schema.Union( +export const NmdWritablePropertyValue = Schema.Union([ Schema.TaggedStruct('title', { value: Schema.String }), Schema.TaggedStruct('rich_text', { value: Schema.NullOr(Schema.String) }), Schema.TaggedStruct('number', { value: Schema.NullOr(Schema.Number) }), @@ -448,7 +446,7 @@ export const NmdWritablePropertyValue = Schema.Union( Schema.TaggedStruct('relation', { value: Schema.Array(NotionUUID) }), Schema.TaggedStruct('place', { value: Schema.NullOr(NmdPlaceValue) }), Schema.TaggedStruct('verification', { value: NmdVerificationValue }), -).annotations({ identifier: 'NotionMd.WritablePropertyValue' }) +]).annotate({ identifier: 'NotionMd.WritablePropertyValue' }) export type NmdWritablePropertyValue = typeof NmdWritablePropertyValue.Type @@ -481,7 +479,7 @@ const NmdFrontmatterBody = Schema.Struct({ url: Schema.optional(Schema.NullOr(Schema.String)), parent: NmdParentRef, page: NmdPageState, - properties: Schema.Record({ key: Schema.String, value: NmdWritablePropertyValue }), + properties: Schema.Record(Schema.String, NmdWritablePropertyValue), /** * Optional compact, non-authoritative property identity hints (R09–R14). * Keyed by visible property name; each descriptor carries the stable @@ -507,7 +505,7 @@ const NmdFrontmatterBody = Schema.Struct({ /** Top-level `.nmd` frontmatter envelope (V2): the single `notion_md` body carrying version/identity/source and the page snapshot. */ export const NmdFrontmatterV2 = Schema.Struct({ notion_md: NmdFrontmatterBody, -}).annotations({ identifier: 'NotionMd.FrontmatterV2' }) +}).annotate({ identifier: 'NotionMd.FrontmatterV2' }) export type NmdFrontmatterV2 = typeof NmdFrontmatterV2.Type @@ -522,15 +520,15 @@ export const NmdSyncStateV1 = Schema.Struct({ page_id: NotionUUID, body: NmdBodyState, storage: NmdStorage, - read_only_properties: Schema.Record({ - key: Schema.String, - value: Schema.Struct({ + read_only_properties: Schema.Record( + Schema.String, + Schema.Struct({ property_type: Schema.String, value: Schema.Unknown, }), - }), + ), data_source: Schema.NullOr(NmdDataSourceBinding), -}).annotations({ identifier: 'NotionMd.SyncStateV1' }) +}).annotate({ identifier: 'NotionMd.SyncStateV1' }) export type NmdSyncStateV1 = typeof NmdSyncStateV1.Type @@ -599,7 +597,7 @@ export type NmdLocalState = } /** Reason a frontmatter + sidecar pair violates the statelessness gate. */ -export class NmdStatelessnessError extends Schema.TaggedError()( +export class NmdStatelessnessError extends Schema.TaggedErrorClass()( 'NotionMd.StatelessnessError', { source: NmdSource, diff --git a/packages/@overeng/notion-effect-client/src/pages.ts b/packages/@overeng/notion-effect-client/src/pages.ts index 687a45f94a..8722202cc0 100644 --- a/packages/@overeng/notion-effect-client/src/pages.ts +++ b/packages/@overeng/notion-effect-client/src/pages.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect, Option, Schema } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { type Page, @@ -144,9 +144,9 @@ const MarkdownContentUpdateSchema = Schema.Struct({ old_str: Schema.String, new_str: Schema.String, replace_all_matches: Schema.optional(Schema.Boolean), -}).annotations({ identifier: 'NotionPages.MarkdownContentUpdate' }) +}).annotate({ identifier: 'NotionPages.MarkdownContentUpdate' }) -const UpdateMarkdownRequestSchema = Schema.Union( +const UpdateMarkdownRequestSchema = Schema.Union([ Schema.Struct({ type: Schema.Literal('update_content'), update_content: Schema.Struct({ @@ -161,7 +161,7 @@ const UpdateMarkdownRequestSchema = Schema.Union( allow_deleting_content: Schema.optional(Schema.Boolean), }), }), -).annotations({ identifier: 'NotionPages.UpdateMarkdownRequest' }) +]).annotate({ identifier: 'NotionPages.UpdateMarkdownRequest' }) const decodeUpdateMarkdownRequest = Schema.decodeUnknown(UpdateMarkdownRequestSchema) diff --git a/packages/@overeng/notion-effect-client/src/schema-helpers.ts b/packages/@overeng/notion-effect-client/src/schema-helpers.ts index bd9a797cc8..1e925d0b69 100644 --- a/packages/@overeng/notion-effect-client/src/schema-helpers.ts +++ b/packages/@overeng/notion-effect-client/src/schema-helpers.ts @@ -22,7 +22,7 @@ import { type SchemaSource = DatabaseSchema | DataSourceSchema /** Error thrown when database schema doesn't match expected property types */ -export class SchemaMismatchError extends Schema.TaggedError()( +export class SchemaMismatchError extends Schema.TaggedErrorClass()( 'SchemaMismatchError', { databaseId: Schema.String, @@ -38,7 +38,7 @@ export class SchemaMismatchError extends Schema.TaggedError ) {} /** Error thrown when schema fields lack required Notion property metadata annotations */ -export class SchemaMetaMissingError extends Schema.TaggedError()( +export class SchemaMetaMissingError extends Schema.TaggedErrorClass()( 'SchemaMetaMissingError', { message: Schema.String, @@ -193,7 +193,7 @@ export const getRequiredPropertiesFromSchema = Effect.fn( continue } - const annotation = SchemaAST.getAnnotation(prop.type, notionPropertyMeta) + const annotation = SchemaAST.resolveAt(notionPropertyMeta)(prop.type) if (Option.isSome(annotation) === true) { required.push({ name: prop.name, tag: annotation.value._tag }) } else { diff --git a/packages/@overeng/notion-effect-client/src/schema-helpers.unit.test.ts b/packages/@overeng/notion-effect-client/src/schema-helpers.unit.test.ts index e785ba110c..272dcd7064 100644 --- a/packages/@overeng/notion-effect-client/src/schema-helpers.unit.test.ts +++ b/packages/@overeng/notion-effect-client/src/schema-helpers.unit.test.ts @@ -162,7 +162,7 @@ Vitest.describe('SchemaHelpers', () => { yield* SchemaHelpers.validatePropertiesFromSchema({ schema: Schema.Struct({ - Name: NotionSchema.title.annotations({ + Name: NotionSchema.title.annotate({ [notionPropertyMeta]: { _tag: 'title', id: 'prop-name', @@ -170,7 +170,7 @@ Vitest.describe('SchemaHelpers', () => { description: null, }, }), - Amount: NotionSchema.numberOption.annotations({ + Amount: NotionSchema.numberOption.annotate({ [notionPropertyMeta]: { _tag: 'number', id: 'prop-amount', diff --git a/packages/@overeng/notion-effect-client/src/search.ts b/packages/@overeng/notion-effect-client/src/search.ts index 0a807d75a5..4caafbd54f 100644 --- a/packages/@overeng/notion-effect-client/src/search.ts +++ b/packages/@overeng/notion-effect-client/src/search.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Chunk, Effect, Option, Schema, Stream } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import type { NotionConfig } from './config.ts' import type { NotionApiError } from './error.ts' @@ -17,9 +17,9 @@ import { /** Search result can be a page or data source */ const SearchResultSchema = Schema.Struct({ - object: Schema.Literal('page', 'data_source'), + object: Schema.Literals(['page', 'data_source']), id: Schema.String, -}).annotations({ identifier: 'SearchResult' }) +}).annotate({ identifier: 'SearchResult' }) type SearchResult = typeof SearchResultSchema.Type diff --git a/packages/@overeng/notion-effect-client/src/test/integration/setup.ts b/packages/@overeng/notion-effect-client/src/test/integration/setup.ts index 25e4972fea..378ed55327 100644 --- a/packages/@overeng/notion-effect-client/src/test/integration/setup.ts +++ b/packages/@overeng/notion-effect-client/src/test/integration/setup.ts @@ -1,5 +1,5 @@ -import { FetchHttpClient, type HttpClient } from '@effect/platform' import { Effect, Layer, Redacted } from 'effect' +import { FetchHttpClient, type HttpClient } from 'effect/unstable/http' import { NotionBlocks } from '../../blocks.ts' import { NotionConfig } from '../../config.ts' diff --git a/packages/@overeng/notion-effect-client/src/test/otelite-span-shape.test.ts b/packages/@overeng/notion-effect-client/src/test/otelite-span-shape.test.ts index 4eacdb999a..0ce1c82f7d 100644 --- a/packages/@overeng/notion-effect-client/src/test/otelite-span-shape.test.ts +++ b/packages/@overeng/notion-effect-client/src/test/otelite-span-shape.test.ts @@ -21,9 +21,9 @@ * 401, not 200). */ -import { HttpClient, HttpClientResponse } from '@effect/platform' import { expect, layer } from '@effect/vitest' import { Effect, Layer, Redacted } from 'effect' +import { HttpClient, HttpClientResponse } from 'effect/unstable/http' import { flushCaptureSpans, diff --git a/packages/@overeng/notion-effect-client/src/test/test-utils.ts b/packages/@overeng/notion-effect-client/src/test/test-utils.ts index 130936fc49..5b4291ccef 100644 --- a/packages/@overeng/notion-effect-client/src/test/test-utils.ts +++ b/packages/@overeng/notion-effect-client/src/test/test-utils.ts @@ -1,10 +1,10 @@ -import { HttpClient, type HttpClientRequest, HttpClientResponse } from '@effect/platform' import { Chunk, Effect, Layer, Redacted, Schema, Stream } from 'effect' +import { HttpClient, type HttpClientRequest, HttpClientResponse } from 'effect/unstable/http' import { type NotionClientConfig, NotionConfig } from '../config.ts' /** Encodes an arbitrary mock body to a JSON string (mirrors `JSON.stringify`). */ -const encodeJson = Schema.encodeSync(Schema.parseJson(Schema.Unknown)) +const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)) /** Mock response configuration */ export interface MockResponse { diff --git a/packages/@overeng/notion-effect-client/src/typed-page.ts b/packages/@overeng/notion-effect-client/src/typed-page.ts index 3b81bf777b..1c12f892a8 100644 --- a/packages/@overeng/notion-effect-client/src/typed-page.ts +++ b/packages/@overeng/notion-effect-client/src/typed-page.ts @@ -30,11 +30,11 @@ export interface TypedPage { /** * Error when decoding page properties fails. */ -export class PageDecodeError extends Schema.TaggedError()('PageDecodeError', { +export class PageDecodeError extends Schema.TaggedErrorClass()('PageDecodeError', { /** Page ID that failed to decode */ pageId: Schema.String, /** The underlying parse error */ - cause: Schema.Defect, + cause: Schema.Defect(), /** Human-readable message */ message: Schema.String, }) {} diff --git a/packages/@overeng/notion-effect-client/src/users.ts b/packages/@overeng/notion-effect-client/src/users.ts index b463d33d8d..c07a1d801c 100644 --- a/packages/@overeng/notion-effect-client/src/users.ts +++ b/packages/@overeng/notion-effect-client/src/users.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Chunk, Effect, Option, Schema, Stream } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import type { NotionConfig } from './config.ts' import type { NotionApiError } from './error.ts' @@ -19,7 +19,7 @@ import { const UserSchema = Schema.Struct({ object: Schema.Literal('user'), id: Schema.String, -}).annotations({ identifier: 'User' }) +}).annotate({ identifier: 'User' }) type User = typeof UserSchema.Type diff --git a/packages/@overeng/notion-effect-client/src/views.ts b/packages/@overeng/notion-effect-client/src/views.ts index 1f4d673578..b960c361e7 100644 --- a/packages/@overeng/notion-effect-client/src/views.ts +++ b/packages/@overeng/notion-effect-client/src/views.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect, Option, Schema, type Stream } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { type View, type ViewType, ViewSchema } from '@overeng/notion-effect-schema' diff --git a/packages/@overeng/notion-effect-schema/package.json b/packages/@overeng/notion-effect-schema/package.json index 555c6bb603..7dcb73c8af 100644 --- a/packages/@overeng/notion-effect-schema/package.json +++ b/packages/@overeng/notion-effect-schema/package.json @@ -16,15 +16,15 @@ "@overeng/notion-core": "workspace:^" }, "devDependencies": { - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/notion-effect-schema/src/common.ts b/packages/@overeng/notion-effect-schema/src/common.ts index 32dbf22ba6..fdb8bec628 100644 --- a/packages/@overeng/notion-effect-schema/src/common.ts +++ b/packages/@overeng/notion-effect-schema/src/common.ts @@ -22,7 +22,7 @@ import { * * @example * ```ts - * Schema.Struct({ ... }).annotations({ + * Schema.Struct({ ... }).annotate({ * [docsPath]: 'property-value-object#title', * }) * ``` @@ -54,7 +54,7 @@ export const resolveDocsUrl = (path: string): string => `${NOTION_DOCS_BASE}/${p * * @see https://developers.notion.com/reference/intro#conventions */ -export const NotionUUID = Schema.String.annotations({ +export const NotionUUID = Schema.String.annotate({ identifier: 'Notion.UUID', title: 'Notion UUID', description: 'A unique identifier in UUID format used throughout the Notion API.', @@ -87,7 +87,7 @@ export const notionObjectUrl = (id: string): string => notionObjectUrlCore(id) * * @see https://developers.notion.com/reference/intro#conventions */ -export const ISO8601DateTime = Schema.String.annotations({ +export const ISO8601DateTime = Schema.String.annotate({ identifier: 'Notion.ISO8601DateTime', title: 'ISO 8601 DateTime', description: 'A timestamp in ISO 8601 format.', @@ -102,7 +102,7 @@ export type ISO8601DateTime = typeof ISO8601DateTime.Type * * @see https://developers.notion.com/reference/rich-text#the-annotation-object */ -export const NotionColor = Schema.Literal(...NOTION_COLORS).annotations({ +export const NotionColor = Schema.Literal(...NOTION_COLORS).annotate({ identifier: 'Notion.Color', title: 'Notion Color', description: 'Color values used for text annotations and backgrounds.', @@ -116,7 +116,7 @@ export type NotionColor = typeof NotionColor.Type * * @see https://developers.notion.com/reference/property-value-object#select */ -export const SelectColor = Schema.Literal(...SELECT_COLORS).annotations({ +export const SelectColor = Schema.Literal(...SELECT_COLORS).annotate({ identifier: 'Notion.SelectColor', title: 'Select Color', description: 'Color values used for select and multi-select options.', @@ -132,7 +132,7 @@ export type SelectColor = typeof SelectColor.Type * * @see https://developers.notion.com/reference/icon-object */ -export const NoticonColor = Schema.Literal(...NOTICON_COLORS).annotations({ +export const NoticonColor = Schema.Literal(...NOTICON_COLORS).annotate({ identifier: 'Notion.NoticonColor', title: 'Noticon Color', description: 'Color values used for native Notion icons (noticons).', @@ -147,7 +147,7 @@ export type NoticonColor = typeof NoticonColor.Type * Can be used in date filters where absolute dates are accepted, * e.g. `{ property: "Due Date", date: { on_or_after: "today" } }`. */ -export const RelativeDate = Schema.Literal( +export const RelativeDate = Schema.Literals([ 'today', 'tomorrow', 'yesterday', @@ -155,7 +155,7 @@ export const RelativeDate = Schema.Literal( 'one_week_from_now', 'one_month_ago', 'one_month_from_now', -).annotations({ +]).annotate({ identifier: 'Notion.RelativeDate', title: 'Relative Date', description: 'Relative date values for database query filters.', @@ -198,9 +198,8 @@ export const shouldNeverHappen = (msg?: string, ...args: unknown[]): never => { const getOptionValueSchema = ( schema: Schema.Schema, TInput, TContext>, ): Schema.Schema => { - const annotated = SchemaAST.getAnnotation>( + const annotated = SchemaAST.resolveAt>(optionValueSchema)( schema.ast, - optionValueSchema, ) if (Option.isSome(annotated) === true) { @@ -215,9 +214,8 @@ const getOptionValueSchema = ( const getOptionNameSchema = ( schema: Schema.Schema, ): Schema.Schema => { - const annotated = SchemaAST.getAnnotation>( + const annotated = SchemaAST.resolveAt>(optionNameSchema)( schema.ast, - optionNameSchema, ) if (Option.isSome(annotated) === true) { @@ -234,14 +232,14 @@ export const withOptionValueSchema = (options: { schema: Schema.Schema, TInput, TContext> valueSchema: Schema.Schema }): Schema.Schema, TInput, TContext> => - options.schema.annotations({ [optionValueSchema]: options.valueSchema }) + options.schema.annotate({ [optionValueSchema]: options.valueSchema }) /** Annotates a schema with the name schema for select/status option extraction */ export const withOptionNameSchema = (options: { schema: Schema.Schema nameSchema: Schema.Schema }): Schema.Schema => - options.schema.annotations({ [optionNameSchema]: options.nameSchema }) + options.schema.annotate({ [optionNameSchema]: options.nameSchema }) /** * Convert select/status `Option` values to `Option`. diff --git a/packages/@overeng/notion-effect-schema/src/objects.ts b/packages/@overeng/notion-effect-schema/src/objects.ts index 38a668e7f7..829fa4274d 100644 --- a/packages/@overeng/notion-effect-schema/src/objects.ts +++ b/packages/@overeng/notion-effect-schema/src/objects.ts @@ -9,7 +9,7 @@ import { PartialUser } from './users.ts' // ----------------------------------------------------------------------------- /** Parent reference for a database */ -export const DatabaseParent = Schema.Union( +export const DatabaseParent = Schema.Union([ Schema.Struct({ type: Schema.Literal('page_id'), page_id: NotionUUID, @@ -22,7 +22,7 @@ export const DatabaseParent = Schema.Union( type: Schema.Literal('workspace'), workspace: Schema.Literal(true), }), -).annotations({ +]).annotate({ identifier: 'Notion.DatabaseParent', [docsPath]: 'database#database-parent', }) @@ -30,7 +30,7 @@ export const DatabaseParent = Schema.Union( export type DatabaseParent = typeof DatabaseParent.Type /** Parent reference for a page */ -export const PageParent = Schema.Union( +export const PageParent = Schema.Union([ Schema.Struct({ type: Schema.Literal('database_id'), database_id: NotionUUID, @@ -60,7 +60,7 @@ export const PageParent = Schema.Union( type: Schema.Literal('agent_id'), agent_id: NotionUUID, }), -).annotations({ +]).annotate({ identifier: 'Notion.PageParent', [docsPath]: 'page#page-parent', }) @@ -68,7 +68,7 @@ export const PageParent = Schema.Union( export type PageParent = typeof PageParent.Type /** Parent reference for a block */ -export const BlockParent = Schema.Union( +export const BlockParent = Schema.Union([ Schema.Struct({ type: Schema.Literal('database_id'), database_id: NotionUUID, @@ -89,7 +89,7 @@ export const BlockParent = Schema.Union( type: Schema.Literal('workspace'), workspace: Schema.Literal(true), }), -).annotations({ +]).annotate({ identifier: 'Notion.BlockParent', [docsPath]: 'block#block-parent', }) @@ -106,7 +106,7 @@ export const ExternalFile = Schema.Struct({ external: Schema.Struct({ url: Schema.String, }), -}).annotations({ +}).annotate({ identifier: 'Notion.ExternalFile', [docsPath]: 'file-object', }) @@ -120,7 +120,7 @@ export const NotionFile = Schema.Struct({ url: Schema.String, expiry_time: ISO8601DateTime, }), -}).annotations({ +}).annotate({ identifier: 'Notion.NotionFile', [docsPath]: 'file-object', }) @@ -128,7 +128,7 @@ export const NotionFile = Schema.Struct({ export type NotionFile = typeof NotionFile.Type /** File object (external or Notion-hosted) */ -export const FileObject = Schema.Union(ExternalFile, NotionFile).annotations({ +export const FileObject = Schema.Union([ExternalFile, NotionFile]).annotate({ identifier: 'Notion.FileObject', [docsPath]: 'file-object', }) @@ -139,7 +139,7 @@ export type FileObject = typeof FileObject.Type export const EmojiIcon = Schema.Struct({ type: Schema.Literal('emoji'), emoji: Schema.String, -}).annotations({ +}).annotate({ identifier: 'Notion.EmojiIcon', [docsPath]: 'emoji-object', }) @@ -154,7 +154,7 @@ export const CustomEmojiIcon = Schema.Struct({ name: Schema.optional(Schema.String), url: Schema.optional(Schema.String), }), -}).annotations({ +}).annotate({ identifier: 'Notion.CustomEmojiIcon', }) @@ -167,7 +167,7 @@ export const NamedIcon = Schema.Struct({ name: Schema.String, color: NoticonColor, }), -}).annotations({ +}).annotate({ identifier: 'Notion.NamedIcon', [docsPath]: 'icon-object', }) @@ -175,13 +175,13 @@ export const NamedIcon = Schema.Struct({ export type NamedIcon = typeof NamedIcon.Type /** Icon (emoji, custom emoji, named icon, external file, or Notion file) */ -export const Icon = Schema.Union( +export const Icon = Schema.Union([ EmojiIcon, CustomEmojiIcon, NamedIcon, ExternalFile, NotionFile, -).annotations({ +]).annotate({ identifier: 'Notion.Icon', }) @@ -195,7 +195,7 @@ export type Icon = typeof Icon.Type export const DataSourceRef = Schema.Struct({ id: NotionUUID, name: Schema.optional(Schema.String), -}).annotations({ +}).annotate({ identifier: 'Notion.DataSourceRef', [docsPath]: 'data-source', }) @@ -206,7 +206,7 @@ export type DataSourceRef = typeof DataSourceRef.Type export const DataSourceParent = Schema.Struct({ type: Schema.Literal('database_id'), database_id: NotionUUID, -}).annotations({ +}).annotate({ identifier: 'Notion.DataSourceParent', }) @@ -229,7 +229,7 @@ export const DataSourceSchema = Schema.Struct({ /** The top-level parent of the owning database (page, block, or workspace) */ database_parent: DatabaseParent, /** Property schema definitions */ - properties: Schema.Record({ key: Schema.String, value: Schema.Unknown }), + properties: Schema.Record(Schema.String, Schema.Unknown), is_inline: Schema.Boolean, in_trash: Schema.Boolean, url: Schema.String, @@ -238,7 +238,7 @@ export const DataSourceSchema = Schema.Struct({ created_by: PartialUser, last_edited_time: ISO8601DateTime, last_edited_by: PartialUser, -}).annotations({ +}).annotate({ identifier: 'Notion.DataSourceSchema', [docsPath]: 'data-source', }) @@ -271,8 +271,8 @@ export const DatabaseSchema = Schema.Struct({ /** Data sources (collections) within the database */ data_sources: Schema.optional(Schema.Array(DataSourceRef)), /** Property schema definitions - moved to data source level but may still appear */ - properties: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), -}).annotations({ + properties: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), +}).annotate({ identifier: 'Notion.DatabaseSchema', [docsPath]: 'database', }) @@ -303,8 +303,8 @@ export const Page = Schema.Struct({ url: Schema.String, public_url: Schema.NullOr(Schema.String), /** Page properties - structure depends on parent type */ - properties: Schema.Record({ key: Schema.String, value: Schema.Unknown }), -}).annotations({ + properties: Schema.Record(Schema.String, Schema.Unknown), +}).annotate({ identifier: 'Notion.Page', [docsPath]: 'page', }) @@ -318,8 +318,8 @@ export const PagePropertyItem = Schema.extend( id: Schema.String, type: Schema.String, }), - Schema.Record({ key: Schema.String, value: Schema.Unknown }), -).annotations({ + Schema.Record(Schema.String, Schema.Unknown), +).annotate({ identifier: 'Notion.PagePropertyItem', [docsPath]: 'page-property-item', }) @@ -333,17 +333,17 @@ export const PagePropertyItemList = Schema.Struct({ results: Schema.Array(PagePropertyItem), next_cursor: Schema.NullOr(Schema.String), has_more: Schema.Boolean, -}).annotations({ +}).annotate({ identifier: 'Notion.PagePropertyItemList', [docsPath]: 'retrieve-a-page-property', }) export type PagePropertyItemList = typeof PagePropertyItemList.Type /** Tagged union covering both shapes the page-property endpoint can return — a single `PagePropertyItem` or a paginated `PagePropertyItemList`. */ -export const PagePropertyItemResponse = Schema.Union( +export const PagePropertyItemResponse = Schema.Union([ PagePropertyItem, PagePropertyItemList, -).annotations({ +]).annotate({ identifier: 'Notion.PagePropertyItemResponse', [docsPath]: 'retrieve-a-page-property', }) @@ -354,7 +354,7 @@ export type PagePropertyItemResponse = typeof PagePropertyItemResponse.Type // ----------------------------------------------------------------------------- /** All supported block types */ -export const BlockType = Schema.Literal( +export const BlockType = Schema.Literals([ // Text & Content 'paragraph', 'heading_1', @@ -363,21 +363,18 @@ export const BlockType = Schema.Literal( 'heading_4', 'quote', 'callout', - 'code', - // Lists + 'code', // Lists 'bulleted_list_item', 'numbered_list_item', 'to_do', - 'toggle', - // Media + 'toggle', // Media 'image', 'video', 'audio', 'file', 'pdf', 'embed', - 'bookmark', - // Organization + 'bookmark', // Organization 'table', 'table_row', 'column_list', @@ -385,8 +382,7 @@ export const BlockType = Schema.Literal( 'divider', 'table_of_contents', 'breadcrumb', - 'tab', - // Advanced + 'tab', // Advanced 'synced_block', 'child_page', 'child_database', @@ -394,10 +390,9 @@ export const BlockType = Schema.Literal( 'template', 'link_preview', 'link_to_page', - 'meeting_notes', - // System + 'meeting_notes', // System 'unsupported', -).annotations({ +]).annotate({ identifier: 'Notion.BlockType', [docsPath]: 'block#block-types', }) @@ -431,8 +426,8 @@ const BlockBase = Schema.Struct({ */ export const Block = Schema.extend( BlockBase, - Schema.Record({ key: Schema.String, value: Schema.Unknown }), -).annotations({ + Schema.Record(Schema.String, Schema.Unknown), +).annotate({ identifier: 'Notion.Block', [docsPath]: 'block', }) @@ -444,7 +439,7 @@ export type Block = typeof Block.Type // ----------------------------------------------------------------------------- /** Rich text array accepted in Notion block create payloads. */ -export const RichTextCreateArray = Schema.Array(TextRichTextCreate).annotations({ +export const RichTextCreateArray = Schema.Array(TextRichTextCreate).annotate({ identifier: 'Notion.RichTextCreateArray', [docsPath]: 'rich-text', }) @@ -541,7 +536,7 @@ export const TableRowBlockCreate = Schema.Struct({ table_row: Schema.Struct({ cells: Schema.Array(RichTextCreateArray), }), -}).annotations({ +}).annotate({ identifier: 'Notion.TableRowBlockCreate', [docsPath]: 'block#table-row', }) @@ -552,7 +547,7 @@ const TableBlockCreate = Schema.Struct({ object: Schema.Literal('block'), type: Schema.Literal('table'), table: Schema.Struct({ - table_width: Schema.NonNegativeInt, + table_width: Schema.Natural, has_column_header: Schema.Boolean, has_row_header: Schema.Boolean, children: Schema.Array(TableRowBlockCreate), @@ -600,7 +595,7 @@ type NotionBlockCreateType = * payloads omit ids, timestamps, parents, and other server-populated fields. */ export const NotionBlockCreate: Schema.Schema = Schema.suspend(() => - Schema.Union( + Schema.Union([ ParagraphBlockCreate, Heading1BlockCreate, Heading2BlockCreate, @@ -612,7 +607,7 @@ export const NotionBlockCreate: Schema.Schema = Schema.su CodeBlockCreate, DividerBlockCreate, TableBlockCreate, - ).annotations({ + ]).annotate({ identifier: 'Notion.BlockCreate', [docsPath]: 'block', }), @@ -634,7 +629,7 @@ export const PageMarkdown = Schema.Struct({ markdown: Schema.String, truncated: Schema.Boolean, unknown_block_ids: Schema.Array(Schema.String), -}).annotations({ +}).annotate({ identifier: 'Notion.PageMarkdown', }) @@ -645,7 +640,7 @@ export type PageMarkdown = typeof PageMarkdown.Type // ----------------------------------------------------------------------------- /** Parent reference for a comment */ -export const CommentParent = Schema.Union( +export const CommentParent = Schema.Union([ Schema.Struct({ type: Schema.Literal('page_id'), page_id: NotionUUID, @@ -654,7 +649,7 @@ export const CommentParent = Schema.Union( type: Schema.Literal('block_id'), block_id: NotionUUID, }), -).annotations({ +]).annotate({ identifier: 'Notion.CommentParent', }) @@ -674,7 +669,7 @@ export const Comment = Schema.Struct({ created_time: ISO8601DateTime, created_by: PartialUser, last_edited_time: ISO8601DateTime, -}).annotations({ +}).annotate({ identifier: 'Notion.Comment', [docsPath]: 'comment-object', }) @@ -686,7 +681,7 @@ export type Comment = typeof Comment.Type // ----------------------------------------------------------------------------- /** Database view types */ -export const ViewType = Schema.Literal( +export const ViewType = Schema.Literals([ 'table', 'board', 'list', @@ -695,7 +690,7 @@ export const ViewType = Schema.Literal( 'gallery', 'form', 'chart', -).annotations({ +]).annotate({ identifier: 'Notion.ViewType', }) @@ -730,7 +725,7 @@ export const View = Schema.Struct({ sorts: Schema.optional(Schema.NullOr(Schema.Unknown)), quick_filters: Schema.optional(Schema.NullOr(Schema.Unknown)), configuration: Schema.optional(Schema.Unknown), -}).annotations({ +}).annotate({ identifier: 'Notion.View', [docsPath]: 'view-object', }) diff --git a/packages/@overeng/notion-effect-schema/src/properties.unit.test.ts b/packages/@overeng/notion-effect-schema/src/properties.unit.test.ts index 9a38df29b0..fe292752f2 100644 --- a/packages/@overeng/notion-effect-schema/src/properties.unit.test.ts +++ b/packages/@overeng/notion-effect-schema/src/properties.unit.test.ts @@ -459,7 +459,7 @@ Vitest.describe('Select Property', () => { }) Vitest.describe('NotionSchema.select(...).pipe(NotionSchema.asName)', () => { - const Allowed = Schema.Literal('High', 'Low') + const Allowed = Schema.Literals(['High', 'Low']) Vitest.it.effect('returns Some with allowed name', () => Effect.gen(function* () { @@ -571,7 +571,7 @@ Vitest.describe('MultiSelect Property', () => { }) Vitest.describe('NotionSchema.multiSelect(...).pipe(NotionSchema.asNames)', () => { - const Allowed = Schema.Literal('Tag1', 'Tag2') + const Allowed = Schema.Literals(['Tag1', 'Tag2']) Vitest.it.effect('decodes to array of allowed names', () => Effect.gen(function* () { @@ -669,7 +669,7 @@ Vitest.describe('Status Property', () => { }) Vitest.describe('NotionSchema.status(...).pipe(NotionSchema.asName)', () => { - const Allowed = Schema.Literal('In Progress', 'Blocked') + const Allowed = Schema.Literals(['In Progress', 'Blocked']) Vitest.it.effect('returns Some with allowed status name', () => Effect.gen(function* () { diff --git a/packages/@overeng/notion-effect-schema/src/properties/audit.ts b/packages/@overeng/notion-effect-schema/src/properties/audit.ts index 1abdb3e9b3..8500aea2e9 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/audit.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/audit.ts @@ -13,17 +13,17 @@ import { PartialUser } from '../users.ts' * @see https://developers.notion.com/reference/property-value-object#created-time */ export const CreatedTimeProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('created_time').annotations({ + type: Schema.Literal('created_time').annotate({ description: 'Property type identifier.', }), - created_time: Schema.String.annotations({ + created_time: Schema.String.annotate({ description: 'When the page was created (ISO 8601).', examples: ['2024-01-15T10:30:00.000Z'], }), -}).annotations({ +}).annotate({ identifier: 'Notion.CreatedTimeProperty', title: 'Created Time Property', description: 'The creation timestamp (read-only).', @@ -64,16 +64,16 @@ export const CreatedTime = { * @see https://developers.notion.com/reference/property-value-object#created-by */ export const CreatedByProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('created_by').annotations({ + type: Schema.Literal('created_by').annotate({ description: 'Property type identifier.', }), - created_by: PartialUser.annotations({ + created_by: PartialUser.annotate({ description: 'The user who created the page.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.CreatedByProperty', title: 'Created By Property', description: 'The user who created the page (read-only).', @@ -114,17 +114,17 @@ export const CreatedBy = { * @see https://developers.notion.com/reference/property-value-object#last-edited-time */ export const LastEditedTimeProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('last_edited_time').annotations({ + type: Schema.Literal('last_edited_time').annotate({ description: 'Property type identifier.', }), - last_edited_time: Schema.String.annotations({ + last_edited_time: Schema.String.annotate({ description: 'When the page was last edited (ISO 8601).', examples: ['2024-01-15T10:30:00.000Z'], }), -}).annotations({ +}).annotate({ identifier: 'Notion.LastEditedTimeProperty', title: 'Last Edited Time Property', description: 'The last edit timestamp (read-only).', @@ -169,16 +169,16 @@ export const LastEditedTime = { * @see https://developers.notion.com/reference/property-value-object#last-edited-by */ export const LastEditedByProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('last_edited_by').annotations({ + type: Schema.Literal('last_edited_by').annotate({ description: 'Property type identifier.', }), - last_edited_by: PartialUser.annotations({ + last_edited_by: PartialUser.annotate({ description: 'The user who last edited the page.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.LastEditedByProperty', title: 'Last Edited By Property', description: 'The user who last edited the page (read-only).', diff --git a/packages/@overeng/notion-effect-schema/src/properties/boolean.ts b/packages/@overeng/notion-effect-schema/src/properties/boolean.ts index 1c8dc8adc3..1f5260e637 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/boolean.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/boolean.ts @@ -12,16 +12,16 @@ import { docsPath, shouldNeverHappen } from '../common.ts' * @see https://developers.notion.com/reference/property-value-object#checkbox */ export const CheckboxProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('checkbox').annotations({ + type: Schema.Literal('checkbox').annotate({ description: 'Property type identifier.', }), - checkbox: Schema.Boolean.annotations({ + checkbox: Schema.Boolean.annotate({ description: 'The checkbox value (checked or unchecked).', }), -}).annotations({ +}).annotate({ identifier: 'Notion.CheckboxProperty', title: 'Checkbox Property', description: 'A checkbox property value.', @@ -37,7 +37,7 @@ export type CheckboxProperty = typeof CheckboxProperty.Type */ export const CheckboxWrite = Schema.Struct({ checkbox: Schema.Boolean, -}).annotations({ +}).annotate({ identifier: 'Notion.CheckboxWrite', title: 'Checkbox (Write)', description: 'Write payload for a checkbox property (used in page create/update).', @@ -51,7 +51,7 @@ export const CheckboxWriteFromBoolean = Schema.transform(Schema.Boolean, Checkbo strict: false, decode: (checkbox) => ({ checkbox }), encode: (write) => write.checkbox, -}).annotations({ +}).annotate({ identifier: 'Notion.CheckboxWriteFromBoolean', title: 'Checkbox (Write) From Boolean', description: 'Transform a boolean into a checkbox write payload.', diff --git a/packages/@overeng/notion-effect-schema/src/properties/canonical-codec.ts b/packages/@overeng/notion-effect-schema/src/properties/canonical-codec.ts index d622856216..53dc93b953 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/canonical-codec.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/canonical-codec.ts @@ -27,21 +27,21 @@ import { } from './canonical.ts' /** A raw Notion property value could not be projected to canonical form. */ -export class CanonicalDecodeError extends Schema.TaggedError()( +export class CanonicalDecodeError extends Schema.TaggedErrorClass()( 'Notion.CanonicalDecodeError', { propertyType: Schema.String, - reason: Schema.Literal('unsupported_type', 'malformed_payload'), + reason: Schema.Literals(['unsupported_type', 'malformed_payload']), message: Schema.String, }, ) {} /** A canonical value cannot be expressed as a Notion write payload. */ -export class CanonicalEncodeError extends Schema.TaggedError()( +export class CanonicalEncodeError extends Schema.TaggedErrorClass()( 'Notion.CanonicalEncodeError', { tag: Schema.String, - reason: Schema.Literal('computed', 'unsupported_remote_shape'), + reason: Schema.Literals(['computed', 'unsupported_remote_shape']), message: Schema.String, }, ) {} diff --git a/packages/@overeng/notion-effect-schema/src/properties/canonical.ts b/packages/@overeng/notion-effect-schema/src/properties/canonical.ts index b92069016b..078e88072c 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/canonical.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/canonical.ts @@ -29,23 +29,23 @@ import { } from '@overeng/notion-core' /** Branded Notion property ID (stable identifier within a database schema). */ -export const PropertyId = Schema.NonEmptyTrimmedString.pipe( +export const PropertyId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('Notion.PropertyId'), - Schema.annotations({ identifier: 'Notion.PropertyId' }), + Schema.annotate({ identifier: 'Notion.PropertyId' }), ) export type PropertyId = typeof PropertyId.Type /** Branded human-readable Notion property name (mutable; distinct from the stable `PropertyId`). */ -export const PropertyName = Schema.NonEmptyTrimmedString.pipe( +export const PropertyName = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('Notion.PropertyName'), - Schema.annotations({ identifier: 'Notion.PropertyName' }), + Schema.annotate({ identifier: 'Notion.PropertyName' }), ) export type PropertyName = typeof PropertyName.Type /** Branded Notion page ID. */ -export const PageId = Schema.NonEmptyTrimmedString.pipe( +export const PageId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('Notion.PageId'), - Schema.annotations({ identifier: 'Notion.PageId' }), + Schema.annotate({ identifier: 'Notion.PageId' }), ) export type PageId = typeof PageId.Type @@ -56,34 +56,34 @@ export type PageId = typeof PageId.Type * from Notion database/data-source IDs whose surface form is not contractually * UUID here, and the brand already carries the meaning. */ -export const DataSourceId = Schema.NonEmptyTrimmedString.pipe( +export const DataSourceId = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('Notion.DataSourceId'), - Schema.annotations({ identifier: 'Notion.DataSourceId' }), + Schema.annotate({ identifier: 'Notion.DataSourceId' }), ) export type DataSourceId = typeof DataSourceId.Type /** Opaque content-hash string (e.g. `sha256:…`). Computed by the caller, not here. */ -export const CanonicalHash = Schema.String.annotations({ identifier: 'Notion.Canonical.Hash' }) +export const CanonicalHash = Schema.String.annotate({ identifier: 'Notion.Canonical.Hash' }) export type CanonicalHash = typeof CanonicalHash.Type /** Canonical select/multi-select/status option, normalized for stable hash comparison. */ export const CanonicalOptionValue = Schema.TaggedStruct('CanonicalOptionValue', { id: Schema.optional(PropertyId), name: PropertyName, - color: Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'Notion.Canonical.OptionValue' }) + color: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'Notion.Canonical.OptionValue' }) export type CanonicalOptionValue = typeof CanonicalOptionValue.Type /** Canonical file attachment: name plus a stable identity hash used for change detection. */ export const CanonicalFileValue = Schema.TaggedStruct('CanonicalFileValue', { - name: Schema.NonEmptyTrimmedString, + name: Schema.Trimmed.check(Schema.isNonEmpty()), identityHash: CanonicalHash, - externalUrl: Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'Notion.Canonical.FileValue' }) + externalUrl: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'Notion.Canonical.FileValue' }) export type CanonicalFileValue = typeof CanonicalFileValue.Type /** Normalized representation of any Notion property value; the `_tag` discriminates the variant. Computed properties carry only their hash. */ -export const CanonicalPropertyValue = Schema.Union( +export const CanonicalPropertyValue = Schema.Union([ Schema.TaggedStruct('empty', {}), Schema.TaggedStruct('title', { plainText: Schema.String, @@ -114,7 +114,7 @@ export const CanonicalPropertyValue = Schema.Union( pageIds: Schema.Array(PageId), }), Schema.TaggedStruct('people', { - userIds: Schema.Array(Schema.NonEmptyTrimmedString), + userIds: Schema.Array(Schema.Trimmed.check(Schema.isNonEmpty())), }), Schema.TaggedStruct('files', { files: Schema.Array(CanonicalFileValue), @@ -131,17 +131,17 @@ export const CanonicalPropertyValue = Schema.Union( Schema.TaggedStruct('computed', { valueHash: CanonicalHash, }), -).annotations({ identifier: 'Notion.Canonical.PropertyValue' }) +]).annotate({ identifier: 'Notion.Canonical.PropertyValue' }) export type CanonicalPropertyValue = typeof CanonicalPropertyValue.Type /** Every Notion property *type* tag the API can return. */ -export const NotionPropertyType = Schema.Literal(...NOTION_PROPERTY_TYPES).annotations({ +export const NotionPropertyType = Schema.Literal(...NOTION_PROPERTY_TYPES).annotate({ identifier: 'Notion.PropertyType', }) export type NotionPropertyType = typeof NotionPropertyType.Type /** How a property value may be written back to Notion. */ -export const PropertyWriteClass = Schema.Literal(...PROPERTY_WRITE_CLASSES).annotations({ +export const PropertyWriteClass = Schema.Literal(...PROPERTY_WRITE_CLASSES).annotate({ identifier: 'Notion.PropertyWriteClass', }) export type PropertyWriteClass = typeof PropertyWriteClass.Type diff --git a/packages/@overeng/notion-effect-schema/src/properties/common.ts b/packages/@overeng/notion-effect-schema/src/properties/common.ts index cdd3cb0f6a..65240ca9b1 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/common.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/common.ts @@ -13,17 +13,17 @@ import { TextLink } from '../rich-text.ts' * @see https://developers.notion.com/reference/property-value-object#select */ export const SelectOption = Schema.Struct({ - id: NotionUUID.annotations({ + id: NotionUUID.annotate({ description: 'Unique identifier for this option.', }), - name: Schema.String.annotations({ + name: Schema.String.annotate({ description: 'Name of the option as displayed in Notion.', examples: ['High', 'Medium', 'Low'], }), - color: SelectColor.annotations({ + color: SelectColor.annotate({ description: 'Color of the option.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.SelectOption', title: 'Select Option', description: 'An option in a select or multi-select property.', @@ -38,10 +38,10 @@ export type SelectOption = typeof SelectOption.Type * * @see https://developers.notion.com/reference/page#page-property-value */ -export const SelectOptionWrite = Schema.Union( +export const SelectOptionWrite = Schema.Union([ Schema.Struct({ id: NotionUUID }), Schema.Struct({ name: Schema.String }), -).annotations({ +]).annotate({ identifier: 'Notion.SelectOptionWrite', title: 'Select Option (Write)', description: 'A select option reference for write requests.', @@ -65,7 +65,7 @@ export const TextRichTextWrite = Schema.Struct({ content: Schema.String, link: Schema.optional(Schema.NullOr(TextLink)), }), -}).annotations({ +}).annotate({ identifier: 'Notion.TextRichTextWrite', title: 'Text Rich Text (Write)', description: 'Minimal text rich text object accepted in Notion write requests.', diff --git a/packages/@overeng/notion-effect-schema/src/properties/computed.ts b/packages/@overeng/notion-effect-schema/src/properties/computed.ts index 737972ec55..355e005628 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/computed.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/computed.ts @@ -10,7 +10,7 @@ import { DateValue } from './date.ts' /** * Formula result value. */ -export const FormulaValue = Schema.Union( +export const FormulaValue = Schema.Union([ Schema.Struct({ type: Schema.Literal('string'), string: Schema.NullOr(Schema.String), @@ -27,7 +27,7 @@ export const FormulaValue = Schema.Union( type: Schema.Literal('date'), date: Schema.NullOr(DateValue), }), -).annotations({ +]).annotate({ identifier: 'Notion.FormulaValue', title: 'Formula Value', description: 'The computed result of a formula.', @@ -42,16 +42,16 @@ export type FormulaValue = typeof FormulaValue.Type * @see https://developers.notion.com/reference/property-value-object#formula */ export const FormulaProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('formula').annotations({ + type: Schema.Literal('formula').annotate({ description: 'Property type identifier.', }), - formula: FormulaValue.annotations({ + formula: FormulaValue.annotate({ description: 'The computed formula result.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.FormulaProperty', title: 'Formula Property', description: 'A formula property value (read-only, computed).', @@ -155,25 +155,25 @@ export const Formula = { * @see https://developers.notion.com/reference/property-value-object#unique-id */ export const UniqueIdProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('unique_id').annotations({ + type: Schema.Literal('unique_id').annotate({ description: 'Property type identifier.', }), unique_id: Schema.Struct({ - prefix: Schema.NullOr(Schema.String).annotations({ + prefix: Schema.NullOr(Schema.String).annotate({ description: 'Optional text prefix for the ID.', examples: ['TASK', 'BUG'], }), - number: Schema.Number.annotations({ + number: Schema.Number.annotate({ description: 'Auto-incrementing number.', examples: [1, 42, 100], }), - }).annotations({ + }).annotate({ description: 'The unique ID value.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.UniqueIdProperty', title: 'Unique ID Property', description: 'An auto-incrementing unique ID (read-only).', diff --git a/packages/@overeng/notion-effect-schema/src/properties/contact.ts b/packages/@overeng/notion-effect-schema/src/properties/contact.ts index f66dae9e23..9114d6b374 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/contact.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/contact.ts @@ -12,17 +12,17 @@ import { docsPath, shouldNeverHappen, withOptionValueSchema } from '../common.ts * @see https://developers.notion.com/reference/property-value-object#url */ export const UrlProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('url').annotations({ + type: Schema.Literal('url').annotate({ description: 'Property type identifier.', }), - url: Schema.NullOr(Schema.String).annotations({ + url: Schema.NullOr(Schema.String).annotate({ description: 'The URL value, or null if empty.', examples: ['https://example.com'], }), -}).annotations({ +}).annotate({ identifier: 'Notion.UrlProperty', title: 'URL Property', description: 'A URL property value.', @@ -38,7 +38,7 @@ export type UrlProperty = typeof UrlProperty.Type */ export const UrlWrite = Schema.Struct({ url: Schema.NullOr(Schema.String), -}).annotations({ +}).annotate({ identifier: 'Notion.UrlWrite', title: 'URL (Write)', description: 'Write payload for a URL property (used in page create/update).', @@ -52,7 +52,7 @@ export const UrlWriteFromString = Schema.transform(Schema.NullOr(Schema.String), strict: false, decode: (url) => ({ url }), encode: (write) => write.url, -}).annotations({ +}).annotate({ identifier: 'Notion.UrlWriteFromString', title: 'URL (Write) From String', description: 'Transform a URL string (or null) into a URL write payload.', @@ -119,17 +119,17 @@ export const Url = { * @see https://developers.notion.com/reference/property-value-object#email */ export const EmailProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('email').annotations({ + type: Schema.Literal('email').annotate({ description: 'Property type identifier.', }), - email: Schema.NullOr(Schema.String).annotations({ + email: Schema.NullOr(Schema.String).annotate({ description: 'The email address, or null if empty.', examples: ['user@example.com'], }), -}).annotations({ +}).annotate({ identifier: 'Notion.EmailProperty', title: 'Email Property', description: 'An email property value.', @@ -145,7 +145,7 @@ export type EmailProperty = typeof EmailProperty.Type */ export const EmailWrite = Schema.Struct({ email: Schema.NullOr(Schema.String), -}).annotations({ +}).annotate({ identifier: 'Notion.EmailWrite', title: 'Email (Write)', description: 'Write payload for an email property (used in page create/update).', @@ -159,7 +159,7 @@ export const EmailWriteFromString = Schema.transform(Schema.NullOr(Schema.String strict: false, decode: (email) => ({ email }), encode: (write) => write.email, -}).annotations({ +}).annotate({ identifier: 'Notion.EmailWriteFromString', title: 'Email (Write) From String', description: 'Transform an email string (or null) into an email write payload.', @@ -228,17 +228,17 @@ export const Email = { * @see https://developers.notion.com/reference/property-value-object#phone-number */ export const PhoneNumberProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('phone_number').annotations({ + type: Schema.Literal('phone_number').annotate({ description: 'Property type identifier.', }), - phone_number: Schema.NullOr(Schema.String).annotations({ + phone_number: Schema.NullOr(Schema.String).annotate({ description: 'The phone number, or null if empty.', examples: ['+1 555-123-4567'], }), -}).annotations({ +}).annotate({ identifier: 'Notion.PhoneNumberProperty', title: 'Phone Number Property', description: 'A phone number property value.', @@ -254,7 +254,7 @@ export type PhoneNumberProperty = typeof PhoneNumberProperty.Type */ export const PhoneNumberWrite = Schema.Struct({ phone_number: Schema.NullOr(Schema.String), -}).annotations({ +}).annotate({ identifier: 'Notion.PhoneNumberWrite', title: 'Phone Number (Write)', description: 'Write payload for a phone number property (used in page create/update).', @@ -272,7 +272,7 @@ export const PhoneNumberWriteFromString = Schema.transform( decode: (phone_number) => ({ phone_number }), encode: (write) => write.phone_number, }, -).annotations({ +).annotate({ identifier: 'Notion.PhoneNumberWriteFromString', title: 'Phone Number (Write) From String', description: 'Transform a phone number string (or null) into a phone number write payload.', diff --git a/packages/@overeng/notion-effect-schema/src/properties/date.ts b/packages/@overeng/notion-effect-schema/src/properties/date.ts index 35028b42a4..395b853a70 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/date.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/date.ts @@ -12,18 +12,18 @@ import { docsPath, shouldNeverHappen, withOptionValueSchema } from '../common.ts * @see https://developers.notion.com/reference/property-value-object#date */ export const DateValue = Schema.Struct({ - start: Schema.String.annotations({ + start: Schema.String.annotate({ description: 'Start date in ISO 8601 format.', examples: ['2024-01-15', '2024-01-15T10:30:00.000Z'], }), - end: Schema.NullOr(Schema.String).annotations({ + end: Schema.NullOr(Schema.String).annotate({ description: 'End date for date ranges, or null for single dates.', }), - time_zone: Schema.NullOr(Schema.String).annotations({ + time_zone: Schema.NullOr(Schema.String).annotate({ description: 'IANA time zone, or null for dates without time.', examples: ['America/New_York', 'Europe/London'], }), -}).annotations({ +}).annotate({ identifier: 'Notion.DateValue', title: 'Date Value', description: 'A date or date range value.', @@ -38,16 +38,16 @@ export type DateValue = typeof DateValue.Type * @see https://developers.notion.com/reference/property-value-object#date */ export const DateProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('date').annotations({ + type: Schema.Literal('date').annotate({ description: 'Property type identifier.', }), - date: Schema.NullOr(DateValue).annotations({ + date: Schema.NullOr(DateValue).annotate({ description: 'The date value, or null if empty.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.DateProperty', title: 'Date Property', description: 'A date property value.', @@ -66,7 +66,7 @@ export const DateValueWrite = Schema.Struct({ start: Schema.String, end: Schema.optional(Schema.NullOr(Schema.String)), time_zone: Schema.optional(Schema.NullOr(Schema.String)), -}).annotations({ +}).annotate({ identifier: 'Notion.DateValueWrite', title: 'Date Value (Write)', description: 'Date value object accepted in Notion write requests.', @@ -82,7 +82,7 @@ export type DateValueWrite = typeof DateValueWrite.Type */ export const DateWrite = Schema.Struct({ date: Schema.NullOr(DateValueWrite), -}).annotations({ +}).annotate({ identifier: 'Notion.DateWrite', title: 'Date (Write)', description: 'Write payload for a date property (used in page create/update).', @@ -102,7 +102,7 @@ export const DateWriteFromStart = Schema.transform(Schema.String, DateWrite, { return write.date.start }, -}).annotations({ +}).annotate({ identifier: 'Notion.DateWriteFromStart', title: 'Date (Write) From Start', description: 'Transform a start date/time string into a date write payload.', diff --git a/packages/@overeng/notion-effect-schema/src/properties/descriptor.ts b/packages/@overeng/notion-effect-schema/src/properties/descriptor.ts index d478d54fe8..ad413fefc9 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/descriptor.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/descriptor.ts @@ -20,7 +20,9 @@ import { Schema } from 'effect' import { DataSourceId, NotionPropertyType, PropertyId, PropertyName } from './canonical.ts' /** Validated `sha256:` content-hash form shared by the descriptor identity hashes. */ -const Sha256Hash = Schema.NonEmptyTrimmedString.pipe(Schema.pattern(/^sha256:[0-9a-f]{64}$/)) +const Sha256Hash = Schema.Trimmed.check(Schema.isNonEmpty()).check( + Schema.check(Schema.isPattern(/^sha256:[0-9a-f]{64}$/)), +) /** * Identity of a property's current schema *configuration* (options, format, @@ -29,7 +31,7 @@ const Sha256Hash = Schema.NonEmptyTrimmedString.pipe(Schema.pattern(/^sha256:[0- */ export const ConfigHash = Sha256Hash.pipe( Schema.brand('Notion.ConfigHash'), - Schema.annotations({ identifier: 'Notion.ConfigHash' }), + Schema.annotate({ identifier: 'Notion.ConfigHash' }), ) export type ConfigHash = typeof ConfigHash.Type @@ -43,7 +45,7 @@ export type ConfigHash = typeof ConfigHash.Type */ export const SchemaHash = Sha256Hash.pipe( Schema.brand('Notion.SchemaHash'), - Schema.annotations({ identifier: 'Notion.SchemaHash' }), + Schema.annotate({ identifier: 'Notion.SchemaHash' }), ) export type SchemaHash = typeof SchemaHash.Type @@ -59,11 +61,11 @@ export type SchemaHash = typeof SchemaHash.Type * Shared foundation with no Phase 1 consumer: the Phase 3 PropertyWriteCore / * proof providers tag stable-identity evidence with this source. */ -export const PropertyIdentityEvidenceSource = Schema.Union( +export const PropertyIdentityEvidenceSource = Schema.Union([ Schema.TaggedStruct('descriptor', {}), Schema.TaggedStruct('workspace_state', {}), Schema.TaggedStruct('live_schema', {}), -).annotations({ identifier: 'Notion.PropertyIdentityEvidenceSource' }) +]).annotate({ identifier: 'Notion.PropertyIdentityEvidenceSource' }) export type PropertyIdentityEvidenceSource = typeof PropertyIdentityEvidenceSource.Type /** @@ -81,7 +83,7 @@ export const PropertyDescriptor = Schema.Struct({ property_type: NotionPropertyType, data_source_id: DataSourceId, config_hash: ConfigHash, -}).annotations({ identifier: 'Notion.PropertyDescriptor' }) +}).annotate({ identifier: 'Notion.PropertyDescriptor' }) export type PropertyDescriptor = typeof PropertyDescriptor.Type /** @@ -94,10 +96,9 @@ export type PropertyDescriptor = typeof PropertyDescriptor.Type * Phase 3 proof provider against fresh remote schema (spec.md ~207), not at this * schema layer. Decode via {@link decodePropertyDescriptors} to stay fail-closed. */ -export const PropertyDescriptors = Schema.Record({ - key: PropertyName, - value: PropertyDescriptor, -}).annotations({ identifier: 'Notion.PropertyDescriptors' }) +export const PropertyDescriptors = Schema.Record(PropertyName, PropertyDescriptor).annotate({ + identifier: 'Notion.PropertyDescriptors', +}) export type PropertyDescriptors = typeof PropertyDescriptors.Type /** diff --git a/packages/@overeng/notion-effect-schema/src/properties/number.ts b/packages/@overeng/notion-effect-schema/src/properties/number.ts index a2f3f2851a..24df15509b 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/number.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/number.ts @@ -12,17 +12,17 @@ import { docsPath, shouldNeverHappen, withOptionValueSchema } from '../common.ts * @see https://developers.notion.com/reference/property-value-object#number */ export const NumberProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('number').annotations({ + type: Schema.Literal('number').annotate({ description: 'Property type identifier.', }), - number: Schema.NullOr(Schema.Number).annotations({ + number: Schema.NullOr(Schema.Number).annotate({ description: 'The numeric value, or null if empty.', examples: [42, 3.14, null], }), -}).annotations({ +}).annotate({ identifier: 'Notion.NumberProperty', title: 'Number Property', description: 'A number property value.', @@ -38,7 +38,7 @@ export type NumberProperty = typeof NumberProperty.Type */ export const NumberWrite = Schema.Struct({ number: Schema.NullOr(Schema.Number), -}).annotations({ +}).annotate({ identifier: 'Notion.NumberWrite', title: 'Number (Write)', description: 'Write payload for a number property (used in page create/update).', @@ -52,7 +52,7 @@ export const NumberWriteFromNumber = Schema.transform(Schema.NullOr(Schema.Numbe strict: false, decode: (number) => ({ number }), encode: (write) => write.number, -}).annotations({ +}).annotate({ identifier: 'Notion.NumberWriteFromNumber', title: 'Number (Write) From Number', description: 'Transform a number (or null) into a number write payload.', diff --git a/packages/@overeng/notion-effect-schema/src/properties/reference.ts b/packages/@overeng/notion-effect-schema/src/properties/reference.ts index 9228701c5a..b77416ba89 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/reference.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/reference.ts @@ -17,16 +17,16 @@ import { User } from '../users.ts' * @see https://developers.notion.com/reference/property-value-object#people */ export const PeopleProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('people').annotations({ + type: Schema.Literal('people').annotate({ description: 'Property type identifier.', }), - people: Schema.Array(User).annotations({ + people: Schema.Array(User).annotate({ description: 'Array of assigned users.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.PeopleProperty', title: 'People Property', description: 'A people property value.', @@ -47,7 +47,7 @@ export const PeopleWrite = Schema.Struct({ id: NotionUUID, }), ), -}).annotations({ +}).annotate({ identifier: 'Notion.PeopleWrite', title: 'People (Write)', description: 'Write payload for a people property (used in page create/update).', @@ -63,7 +63,7 @@ export const PeopleWriteFromIds = Schema.transform(Schema.Array(NotionUUID), Peo people: ids.map((id) => ({ id })), }), encode: (write) => write.people.map((p) => p.id), -}).annotations({ +}).annotate({ identifier: 'Notion.PeopleWriteFromIds', title: 'People (Write) From IDs', description: 'Transform user IDs into a people write payload.', @@ -111,25 +111,25 @@ export const People = { * @see https://developers.notion.com/reference/property-value-object#relation */ export const RelationProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('relation').annotations({ + type: Schema.Literal('relation').annotate({ description: 'Property type identifier.', }), relation: Schema.Array( Schema.Struct({ - id: NotionUUID.annotations({ + id: NotionUUID.annotate({ description: 'ID of the related page.', }), }), - ).annotations({ + ).annotate({ description: 'Array of related page references.', }), - has_more: Schema.optionalWith(Schema.Boolean, { as: 'Option' }).annotations({ + has_more: Schema.OptionFromOptional(Schema.Boolean).annotate({ description: 'Whether there are more relations than returned.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.RelationProperty', title: 'Relation Property', description: 'A relation property value linking to other pages.', @@ -150,7 +150,7 @@ export const RelationWrite = Schema.Struct({ id: NotionUUID, }), ), -}).annotations({ +}).annotate({ identifier: 'Notion.RelationWrite', title: 'Relation (Write)', description: 'Write payload for a relation property (used in page create/update).', @@ -166,7 +166,7 @@ export const RelationWriteFromIds = Schema.transform(Schema.Array(NotionUUID), R relation: ids.map((id) => ({ id })), }), encode: (write) => write.relation.map((r) => r.id), -}).annotations({ +}).annotate({ identifier: 'Notion.RelationWriteFromIds', title: 'Relation (Write) From IDs', description: 'Transform page IDs into a relation write payload.', @@ -282,11 +282,11 @@ export const Relation = { export const ExternalFile = Schema.extend( ExternalFileReference, Schema.Struct({ - name: Schema.String.annotations({ + name: Schema.String.annotate({ description: 'Name of the file.', }), }), -).annotations({ +).annotate({ identifier: 'Notion.ExternalFile', title: 'External File', description: 'A file hosted externally.', @@ -301,11 +301,11 @@ export type ExternalFile = typeof ExternalFile.Type export const NotionFile = Schema.extend( NotionFileReference, Schema.Struct({ - name: Schema.String.annotations({ + name: Schema.String.annotate({ description: 'Name of the file.', }), }), -).annotations({ +).annotate({ identifier: 'Notion.NotionFile', title: 'Notion File', description: 'A file hosted on Notion (URL expires).', @@ -317,7 +317,7 @@ export type NotionFile = typeof NotionFile.Type /** * File object (either external or Notion-hosted). */ -export const FileObject = Schema.Union(ExternalFile, NotionFile).annotations({ +export const FileObject = Schema.Union([ExternalFile, NotionFile]).annotate({ identifier: 'Notion.FileObject', title: 'File Object', description: 'A file, either external or Notion-hosted.', @@ -332,16 +332,16 @@ export type FileObject = typeof FileObject.Type * @see https://developers.notion.com/reference/property-value-object#files */ export const FilesProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('files').annotations({ + type: Schema.Literal('files').annotate({ description: 'Property type identifier.', }), - files: Schema.Array(FileObject).annotations({ + files: Schema.Array(FileObject).annotate({ description: 'Array of file objects.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.FilesProperty', title: 'Files Property', description: 'A files property value.', @@ -366,7 +366,7 @@ export const FilesWrite = Schema.Struct({ }), }), ), -}).annotations({ +}).annotate({ identifier: 'Notion.FilesWrite', title: 'Files (Write)', description: 'Write payload for a files property (used in page create/update).', @@ -385,7 +385,7 @@ export const FilesWriteFromUrls = Schema.transform(Schema.Array(Schema.String), })), }), encode: (write) => write.files.map((f) => f.external.url), -}).annotations({ +}).annotate({ identifier: 'Notion.FilesWriteFromUrls', title: 'Files (Write) From URLs', description: 'Transform external URLs into a files write payload.', diff --git a/packages/@overeng/notion-effect-schema/src/properties/rollup.ts b/packages/@overeng/notion-effect-schema/src/properties/rollup.ts index 6aac5c4413..2df6712883 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/rollup.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/rollup.ts @@ -10,7 +10,7 @@ import { DateValue } from './date.ts' /** * Rollup result value. */ -export const RollupValue = Schema.Union( +export const RollupValue = Schema.Union([ Schema.Struct({ type: Schema.Literal('string'), string: Schema.NullOr(Schema.String), @@ -35,7 +35,7 @@ export const RollupValue = Schema.Union( type: Schema.Literal('unsupported'), unsupported: Schema.NullOr(Schema.Unknown), }), -).annotations({ +]).annotate({ identifier: 'Notion.RollupValue', title: 'Rollup Value', description: 'The computed result of a rollup.', @@ -50,16 +50,16 @@ export type RollupValue = typeof RollupValue.Type * @see https://developers.notion.com/reference/property-value-object#rollup */ export const RollupProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('rollup').annotations({ + type: Schema.Literal('rollup').annotate({ description: 'Property type identifier.', }), - rollup: RollupValue.annotations({ + rollup: RollupValue.annotate({ description: 'The computed rollup result.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.RollupProperty', title: 'Rollup Property', description: 'A rollup property value (read-only, computed).', diff --git a/packages/@overeng/notion-effect-schema/src/properties/select.ts b/packages/@overeng/notion-effect-schema/src/properties/select.ts index a41244f670..811608440d 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/select.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/select.ts @@ -20,16 +20,16 @@ import { SelectOption, SelectOptionWrite } from './common.ts' * @see https://developers.notion.com/reference/property-value-object#select */ export const SelectProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('select').annotations({ + type: Schema.Literal('select').annotate({ description: 'Property type identifier.', }), - select: Schema.NullOr(SelectOption).annotations({ + select: Schema.NullOr(SelectOption).annotate({ description: 'The selected option, or null if none selected.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.SelectProperty', title: 'Select Property', description: 'A select property value.', @@ -45,7 +45,7 @@ export type SelectProperty = typeof SelectProperty.Type */ export const SelectWrite = Schema.Struct({ select: Schema.NullOr(SelectOptionWrite), -}).annotations({ +}).annotate({ identifier: 'Notion.SelectWrite', title: 'Select (Write)', description: 'Write payload for a select property (used in page create/update).', @@ -71,7 +71,7 @@ export const SelectWriteFromName = Schema.transform(Schema.NullOr(Schema.String) return shouldNeverHappen('SelectWriteFromName cannot encode option referenced by id.') }, -}).annotations({ +}).annotate({ identifier: 'Notion.SelectWriteFromName', title: 'Select (Write) From Name', description: 'Transform an option name (or null) into a select write payload.', @@ -222,16 +222,16 @@ export const Select = { * @see https://developers.notion.com/reference/property-value-object#multi-select */ export const MultiSelectProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('multi_select').annotations({ + type: Schema.Literal('multi_select').annotate({ description: 'Property type identifier.', }), - multi_select: Schema.Array(SelectOption).annotations({ + multi_select: Schema.Array(SelectOption).annotate({ description: 'Array of selected options.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.MultiSelectProperty', title: 'Multi-Select Property', description: 'A multi-select property value.', @@ -247,7 +247,7 @@ export type MultiSelectProperty = typeof MultiSelectProperty.Type */ export const MultiSelectWrite = Schema.Struct({ multi_select: Schema.Array(SelectOptionWrite), -}).annotations({ +}).annotate({ identifier: 'Notion.MultiSelectWrite', title: 'Multi-Select (Write)', description: 'Write payload for a multi-select property (used in page create/update).', @@ -274,7 +274,7 @@ export const MultiSelectWriteFromNames = Schema.transform( return shouldNeverHappen('MultiSelectWriteFromNames cannot encode option referenced by id.') }), }, -).annotations({ +).annotate({ identifier: 'Notion.MultiSelectWriteFromNames', title: 'Multi-Select (Write) From Names', description: 'Transform option names into a multi-select write payload.', @@ -395,16 +395,16 @@ export const MultiSelect = { * @see https://developers.notion.com/reference/property-value-object#status */ export const StatusProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('status').annotations({ + type: Schema.Literal('status').annotate({ description: 'Property type identifier.', }), - status: Schema.NullOr(SelectOption).annotations({ + status: Schema.NullOr(SelectOption).annotate({ description: 'The current status, or null if none.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.StatusProperty', title: 'Status Property', description: 'A status property value.', @@ -420,7 +420,7 @@ export type StatusProperty = typeof StatusProperty.Type */ export const StatusWrite = Schema.Struct({ status: Schema.NullOr(SelectOptionWrite), -}).annotations({ +}).annotate({ identifier: 'Notion.StatusWrite', title: 'Status (Write)', description: 'Write payload for a status property (used in page create/update).', @@ -446,7 +446,7 @@ export const StatusWriteFromName = Schema.transform(Schema.NullOr(Schema.String) return shouldNeverHappen('StatusWriteFromName cannot encode option referenced by id.') }, -}).annotations({ +}).annotate({ identifier: 'Notion.StatusWriteFromName', title: 'Status (Write) From Name', description: 'Transform a status name (or null) into a status write payload.', diff --git a/packages/@overeng/notion-effect-schema/src/properties/text.ts b/packages/@overeng/notion-effect-schema/src/properties/text.ts index 402db03c5f..6e40449840 100644 --- a/packages/@overeng/notion-effect-schema/src/properties/text.ts +++ b/packages/@overeng/notion-effect-schema/src/properties/text.ts @@ -14,16 +14,16 @@ import { TextRichTextWrite } from './common.ts' * @see https://developers.notion.com/reference/property-value-object#title */ export const TitleProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('title').annotations({ + type: Schema.Literal('title').annotate({ description: 'Property type identifier.', }), - title: Schema.Array(RichText).annotations({ + title: Schema.Array(RichText).annotate({ description: 'Title content as rich text array.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.TitleProperty', title: 'Title Property', description: 'The title property of a Notion page.', @@ -39,7 +39,7 @@ export type TitleProperty = typeof TitleProperty.Type */ export const TitleWrite = Schema.Struct({ title: Schema.Array(TextRichTextWrite), -}).annotations({ +}).annotate({ identifier: 'Notion.TitleWrite', title: 'Title (Write)', description: 'Write payload for a title property (used in page create/update).', @@ -55,7 +55,7 @@ export const TitleWriteFromString = Schema.transform(Schema.String, TitleWrite, title: [{ type: 'text', text: { content: str } }], }), encode: (write) => write.title.map((rt) => rt.text.content).join(''), -}).annotations({ +}).annotate({ identifier: 'Notion.TitleWriteFromString', title: 'Title (Write) From String', description: 'Transform a plain string into a title write payload.', @@ -103,16 +103,16 @@ export const Title = { * @see https://developers.notion.com/reference/property-value-object#rich-text */ export const RichTextProperty = Schema.Struct({ - id: Schema.String.annotations({ + id: Schema.String.annotate({ description: 'Property identifier.', }), - type: Schema.Literal('rich_text').annotations({ + type: Schema.Literal('rich_text').annotate({ description: 'Property type identifier.', }), - rich_text: RichTextArray.annotations({ + rich_text: RichTextArray.annotate({ description: 'Content as rich text array.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.RichTextProperty', title: 'Rich Text Property', description: 'A rich text property value.', @@ -128,7 +128,7 @@ export type RichTextProperty = typeof RichTextProperty.Type */ export const RichTextWrite = Schema.Struct({ rich_text: Schema.Array(TextRichTextWrite), -}).annotations({ +}).annotate({ identifier: 'Notion.RichTextWrite', title: 'Rich Text (Write)', description: 'Write payload for a rich text property (used in page create/update).', @@ -144,7 +144,7 @@ export const RichTextWriteFromString = Schema.transform(Schema.String, RichTextW rich_text: [{ type: 'text', text: { content: str } }], }), encode: (write) => write.rich_text.map((rt) => rt.text.content).join(''), -}).annotations({ +}).annotate({ identifier: 'Notion.RichTextWriteFromString', title: 'Rich Text (Write) From String', description: 'Transform a plain string into a rich text write payload.', diff --git a/packages/@overeng/notion-effect-schema/src/property-schema.ts b/packages/@overeng/notion-effect-schema/src/property-schema.ts index 6392bacf3b..67117d2d97 100644 --- a/packages/@overeng/notion-effect-schema/src/property-schema.ts +++ b/packages/@overeng/notion-effect-schema/src/property-schema.ts @@ -12,7 +12,7 @@ export const SelectOptionConfig = Schema.Struct({ name: Schema.String, color: SelectColor, description: Schema.optional(Schema.NullOr(Schema.String)), -}).annotations({ +}).annotate({ identifier: 'Notion.SelectOptionConfig', }) @@ -24,14 +24,14 @@ export const StatusGroupConfig = Schema.Struct({ name: Schema.String, color: SelectColor, option_ids: Schema.Array(NotionUUID), -}).annotations({ +}).annotate({ identifier: 'Notion.StatusGroupConfig', }) export type StatusGroupConfig = typeof StatusGroupConfig.Type /** Number format options */ -export const NumberFormat = Schema.Literal( +export const NumberFormat = Schema.Literals([ 'number', 'number_with_commas', 'percent', @@ -71,14 +71,14 @@ export const NumberFormat = Schema.Literal( 'argentine_peso', 'uruguayan_peso', 'singapore_dollar', -).annotations({ +]).annotate({ identifier: 'Notion.NumberFormat', }) export type NumberFormat = typeof NumberFormat.Type /** Rollup function options */ -export const RollupFunction = Schema.Literal( +export const RollupFunction = Schema.Literals([ 'count', 'count_values', 'empty', @@ -101,7 +101,7 @@ export const RollupFunction = Schema.Literal( 'percent_checked', 'percent_unchecked', 'show_original', -).annotations({ +]).annotate({ identifier: 'Notion.RollupFunction', }) @@ -125,7 +125,7 @@ const PropertySchemaBase = Schema.Struct({ export const TitlePropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('title', {}), -).annotations({ +).annotate({ identifier: 'Notion.TitlePropertySchema', [docsPath]: 'database-property#title', }) @@ -136,7 +136,7 @@ export type TitlePropertySchema = typeof TitlePropertySchema.Type export const RichTextPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('rich_text', {}), -).annotations({ +).annotate({ identifier: 'Notion.RichTextPropertySchema', [docsPath]: 'database-property#rich-text', }) @@ -151,7 +151,7 @@ export const NumberPropertySchema = Schema.extend( format: NumberFormat, }), }), -).annotations({ +).annotate({ identifier: 'Notion.NumberPropertySchema', [docsPath]: 'database-property#number', }) @@ -166,7 +166,7 @@ export const SelectPropertySchema = Schema.extend( options: Schema.Array(SelectOptionConfig), }), }), -).annotations({ +).annotate({ identifier: 'Notion.SelectPropertySchema', [docsPath]: 'database-property#select', }) @@ -181,7 +181,7 @@ export const MultiSelectPropertySchema = Schema.extend( options: Schema.Array(SelectOptionConfig), }), }), -).annotations({ +).annotate({ identifier: 'Notion.MultiSelectPropertySchema', [docsPath]: 'database-property#multi-select', }) @@ -197,7 +197,7 @@ export const StatusPropertySchema = Schema.extend( groups: Schema.Array(StatusGroupConfig), }), }), -).annotations({ +).annotate({ identifier: 'Notion.StatusPropertySchema', [docsPath]: 'database-property#status', }) @@ -208,7 +208,7 @@ export type StatusPropertySchema = typeof StatusPropertySchema.Type export const DatePropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('date', {}), -).annotations({ +).annotate({ identifier: 'Notion.DatePropertySchema', [docsPath]: 'database-property#date', }) @@ -219,7 +219,7 @@ export type DatePropertySchema = typeof DatePropertySchema.Type export const PeoplePropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('people', {}), -).annotations({ +).annotate({ identifier: 'Notion.PeoplePropertySchema', [docsPath]: 'database-property#people', }) @@ -230,7 +230,7 @@ export type PeoplePropertySchema = typeof PeoplePropertySchema.Type export const FilesPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('files', {}), -).annotations({ +).annotate({ identifier: 'Notion.FilesPropertySchema', [docsPath]: 'database-property#files', }) @@ -241,7 +241,7 @@ export type FilesPropertySchema = typeof FilesPropertySchema.Type export const CheckboxPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('checkbox', {}), -).annotations({ +).annotate({ identifier: 'Notion.CheckboxPropertySchema', [docsPath]: 'database-property#checkbox', }) @@ -252,7 +252,7 @@ export type CheckboxPropertySchema = typeof CheckboxPropertySchema.Type export const UrlPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('url', {}), -).annotations({ +).annotate({ identifier: 'Notion.UrlPropertySchema', [docsPath]: 'database-property#url', }) @@ -263,7 +263,7 @@ export type UrlPropertySchema = typeof UrlPropertySchema.Type export const EmailPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('email', {}), -).annotations({ +).annotate({ identifier: 'Notion.EmailPropertySchema', [docsPath]: 'database-property#email', }) @@ -274,7 +274,7 @@ export type EmailPropertySchema = typeof EmailPropertySchema.Type export const PhoneNumberPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('phone_number', {}), -).annotations({ +).annotate({ identifier: 'Notion.PhoneNumberPropertySchema', [docsPath]: 'database-property#phone-number', }) @@ -289,7 +289,7 @@ export const FormulaPropertySchema = Schema.extend( expression: Schema.String, }), }), -).annotations({ +).annotate({ identifier: 'Notion.FormulaPropertySchema', [docsPath]: 'database-property#formula', }) @@ -302,7 +302,7 @@ export const RelationPropertySchema = Schema.extend( Schema.TaggedStruct('relation', { relation: Schema.Struct({ database_id: NotionUUID, - type: Schema.Literal('single_property', 'dual_property'), + type: Schema.Literals(['single_property', 'dual_property']), single_property: Schema.optional(Schema.Struct({})), dual_property: Schema.optional( Schema.Struct({ @@ -312,7 +312,7 @@ export const RelationPropertySchema = Schema.extend( ), }), }), -).annotations({ +).annotate({ identifier: 'Notion.RelationPropertySchema', [docsPath]: 'database-property#relation', }) @@ -331,7 +331,7 @@ export const RollupPropertySchema = Schema.extend( function: RollupFunction, }), }), -).annotations({ +).annotate({ identifier: 'Notion.RollupPropertySchema', [docsPath]: 'database-property#rollup', }) @@ -342,7 +342,7 @@ export type RollupPropertySchema = typeof RollupPropertySchema.Type export const CreatedTimePropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('created_time', {}), -).annotations({ +).annotate({ identifier: 'Notion.CreatedTimePropertySchema', [docsPath]: 'database-property#created-time', }) @@ -353,7 +353,7 @@ export type CreatedTimePropertySchema = typeof CreatedTimePropertySchema.Type export const CreatedByPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('created_by', {}), -).annotations({ +).annotate({ identifier: 'Notion.CreatedByPropertySchema', [docsPath]: 'database-property#created-by', }) @@ -364,7 +364,7 @@ export type CreatedByPropertySchema = typeof CreatedByPropertySchema.Type export const LastEditedTimePropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('last_edited_time', {}), -).annotations({ +).annotate({ identifier: 'Notion.LastEditedTimePropertySchema', [docsPath]: 'database-property#last-edited-time', }) @@ -375,7 +375,7 @@ export type LastEditedTimePropertySchema = typeof LastEditedTimePropertySchema.T export const LastEditedByPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('last_edited_by', {}), -).annotations({ +).annotate({ identifier: 'Notion.LastEditedByPropertySchema', [docsPath]: 'database-property#last-edited-by', }) @@ -390,7 +390,7 @@ export const UniqueIdPropertySchema = Schema.extend( prefix: Schema.NullOr(Schema.String), }), }), -).annotations({ +).annotate({ identifier: 'Notion.UniqueIdPropertySchema', [docsPath]: 'database-property#unique-id', }) @@ -401,7 +401,7 @@ export type UniqueIdPropertySchema = typeof UniqueIdPropertySchema.Type export const VerificationPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('verification', {}), -).annotations({ +).annotate({ identifier: 'Notion.VerificationPropertySchema', }) @@ -411,7 +411,7 @@ export type VerificationPropertySchema = typeof VerificationPropertySchema.Type export const ButtonPropertySchema = Schema.extend( PropertySchemaBase, Schema.TaggedStruct('button', {}), -).annotations({ +).annotate({ identifier: 'Notion.ButtonPropertySchema', }) @@ -425,7 +425,7 @@ export type ButtonPropertySchema = typeof ButtonPropertySchema.Type * Union of all database property schema configurations. * Discriminated by the `_tag` field (property type). */ -export const PropertySchema = Schema.Union( +export const PropertySchema = Schema.Union([ TitlePropertySchema, RichTextPropertySchema, NumberPropertySchema, @@ -449,7 +449,7 @@ export const PropertySchema = Schema.Union( UniqueIdPropertySchema, VerificationPropertySchema, ButtonPropertySchema, -).annotations({ +]).annotate({ identifier: 'Notion.PropertySchema', description: 'Database property schema configuration (discriminated union)', }) diff --git a/packages/@overeng/notion-effect-schema/src/rich-text.ts b/packages/@overeng/notion-effect-schema/src/rich-text.ts index 3f2cbb9006..21b8aca62e 100644 --- a/packages/@overeng/notion-effect-schema/src/rich-text.ts +++ b/packages/@overeng/notion-effect-schema/src/rich-text.ts @@ -12,25 +12,25 @@ import { docsPath, NotionColor, NotionUUID } from './common.ts' * @see https://developers.notion.com/reference/rich-text#the-annotation-object */ export const TextAnnotations = Schema.Struct({ - bold: Schema.Boolean.annotations({ + bold: Schema.Boolean.annotate({ description: 'Whether the text is bolded.', }), - italic: Schema.Boolean.annotations({ + italic: Schema.Boolean.annotate({ description: 'Whether the text is italicized.', }), - strikethrough: Schema.Boolean.annotations({ + strikethrough: Schema.Boolean.annotate({ description: 'Whether the text has a strikethrough.', }), - underline: Schema.Boolean.annotations({ + underline: Schema.Boolean.annotate({ description: 'Whether the text is underlined.', }), - code: Schema.Boolean.annotations({ + code: Schema.Boolean.annotate({ description: 'Whether the text is formatted as inline code.', }), - color: NotionColor.annotations({ + color: NotionColor.annotate({ description: 'Color of the text or background.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.TextAnnotations', title: 'Text Annotations', description: 'Styling properties applied to rich text content.', @@ -52,7 +52,7 @@ export const TextAnnotationsCreate = Schema.Struct({ underline: Schema.optional(Schema.Boolean), code: Schema.optional(Schema.Boolean), color: Schema.optional(NotionColor), -}).annotations({ +}).annotate({ identifier: 'Notion.TextAnnotationsCreate', title: 'Text Annotations (Create)', description: 'Optional styling properties accepted in Notion rich text create payloads.', @@ -69,11 +69,11 @@ export type TextAnnotationsCreate = typeof TextAnnotationsCreate.Type * Link object for text content. */ export const TextLink = Schema.Struct({ - url: Schema.String.annotations({ + url: Schema.String.annotate({ description: 'The URL the text links to.', examples: ['https://example.com'], }), -}).annotations({ +}).annotate({ identifier: 'Notion.TextLink', title: 'Text Link', description: 'A hyperlink associated with text content.', @@ -88,26 +88,26 @@ export type TextLink = typeof TextLink.Type * @see https://developers.notion.com/reference/rich-text#text */ export const TextRichText = Schema.Struct({ - type: Schema.Literal('text').annotations({ + type: Schema.Literal('text').annotate({ description: 'Type identifier for text rich text.', }), text: Schema.Struct({ - content: Schema.String.annotations({ + content: Schema.String.annotate({ description: 'The actual text content.', examples: ['Hello, world!'], }), - link: Schema.NullOr(TextLink).annotations({ + link: Schema.NullOr(TextLink).annotate({ description: 'Optional link for the text.', }), }), annotations: TextAnnotations, - plain_text: Schema.String.annotations({ + plain_text: Schema.String.annotate({ description: 'Plain text without styling.', }), - href: Schema.NullOr(Schema.String).annotations({ + href: Schema.NullOr(Schema.String).annotate({ description: 'URL if this text is a link.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.TextRichText', title: 'Text Rich Text', description: 'Rich text content of type text.', @@ -128,7 +128,7 @@ export const TextRichTextCreate = Schema.Struct({ link: Schema.optional(Schema.NullOr(TextLink)), }), annotations: Schema.optional(TextAnnotationsCreate), -}).annotations({ +}).annotate({ identifier: 'Notion.TextRichTextCreate', title: 'Text Rich Text (Create)', description: 'Minimal text rich text object accepted in Notion create/update requests.', @@ -149,7 +149,7 @@ export const DatabaseMention = Schema.Struct({ database: Schema.Struct({ id: NotionUUID, }), -}).annotations({ +}).annotate({ identifier: 'Notion.DatabaseMention', title: 'Database Mention', description: 'A mention referencing a Notion database.', @@ -166,7 +166,7 @@ export const PageMention = Schema.Struct({ page: Schema.Struct({ id: NotionUUID, }), -}).annotations({ +}).annotate({ identifier: 'Notion.PageMention', title: 'Page Mention', description: 'A mention referencing a Notion page.', @@ -184,7 +184,7 @@ export const UserMention = Schema.Struct({ object: Schema.Literal('user'), id: NotionUUID, }), -}).annotations({ +}).annotate({ identifier: 'Notion.UserMention', title: 'User Mention', description: 'A mention referencing a Notion user.', @@ -199,19 +199,19 @@ export type UserMention = typeof UserMention.Type export const DateMention = Schema.Struct({ type: Schema.Literal('date'), date: Schema.Struct({ - start: Schema.String.annotations({ + start: Schema.String.annotate({ description: 'Start date in ISO 8601 format.', examples: ['2024-01-15'], }), - end: Schema.NullOr(Schema.String).annotations({ + end: Schema.NullOr(Schema.String).annotate({ description: 'Optional end date for date ranges.', }), - time_zone: Schema.NullOr(Schema.String).annotations({ + time_zone: Schema.NullOr(Schema.String).annotate({ description: 'Optional IANA time zone.', examples: ['America/New_York'], }), }), -}).annotations({ +}).annotate({ identifier: 'Notion.DateMention', title: 'Date Mention', description: 'A mention referencing a date or date range.', @@ -226,12 +226,12 @@ export type DateMention = typeof DateMention.Type export const LinkPreviewMention = Schema.Struct({ type: Schema.Literal('link_preview'), link_preview: Schema.Struct({ - url: Schema.String.annotations({ + url: Schema.String.annotate({ description: 'The URL being previewed.', examples: ['https://github.com/example/repo'], }), }), -}).annotations({ +}).annotate({ identifier: 'Notion.LinkPreviewMention', title: 'Link Preview Mention', description: 'A mention displaying a link preview.', @@ -245,8 +245,8 @@ export type LinkPreviewMention = typeof LinkPreviewMention.Type */ export const TemplateMentionDate = Schema.Struct({ type: Schema.Literal('template_mention_date'), - template_mention_date: Schema.Literal('today', 'now'), -}).annotations({ + template_mention_date: Schema.Literals(['today', 'now']), +}).annotate({ identifier: 'Notion.TemplateMentionDate', title: 'Template Mention Date', description: 'A template mention for dynamic dates.', @@ -261,7 +261,7 @@ export type TemplateMentionDate = typeof TemplateMentionDate.Type export const TemplateMentionUser = Schema.Struct({ type: Schema.Literal('template_mention_user'), template_mention_user: Schema.Literal('me'), -}).annotations({ +}).annotate({ identifier: 'Notion.TemplateMentionUser', title: 'Template Mention User', description: 'A template mention for the current user.', @@ -275,8 +275,8 @@ export type TemplateMentionUser = typeof TemplateMentionUser.Type */ export const TemplateMention = Schema.Struct({ type: Schema.Literal('template_mention'), - template_mention: Schema.Union(TemplateMentionDate, TemplateMentionUser), -}).annotations({ + template_mention: Schema.Union([TemplateMentionDate, TemplateMentionUser]), +}).annotate({ identifier: 'Notion.TemplateMention', title: 'Template Mention', description: 'A template mention for dynamic content.', @@ -288,14 +288,14 @@ export type TemplateMention = typeof TemplateMention.Type /** * Union of all mention types. */ -export const MentionContent = Schema.Union( +export const MentionContent = Schema.Union([ DatabaseMention, PageMention, UserMention, DateMention, LinkPreviewMention, TemplateMention, -).annotations({ +]).annotate({ identifier: 'Notion.MentionContent', title: 'Mention Content', description: 'The content of a mention, varying by mention type.', @@ -310,18 +310,18 @@ export type MentionContent = typeof MentionContent.Type * @see https://developers.notion.com/reference/rich-text#mention */ export const MentionRichText = Schema.Struct({ - type: Schema.Literal('mention').annotations({ + type: Schema.Literal('mention').annotate({ description: 'Type identifier for mention rich text.', }), mention: MentionContent, annotations: TextAnnotations, - plain_text: Schema.String.annotations({ + plain_text: Schema.String.annotate({ description: 'Plain text representation of the mention.', }), - href: Schema.NullOr(Schema.String).annotations({ + href: Schema.NullOr(Schema.String).annotate({ description: 'URL if this mention links somewhere.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.MentionRichText', title: 'Mention Rich Text', description: 'Rich text content containing an inline mention.', @@ -340,23 +340,23 @@ export type MentionRichText = typeof MentionRichText.Type * @see https://developers.notion.com/reference/rich-text#equation */ export const EquationRichText = Schema.Struct({ - type: Schema.Literal('equation').annotations({ + type: Schema.Literal('equation').annotate({ description: 'Type identifier for equation rich text.', }), equation: Schema.Struct({ - expression: Schema.String.annotations({ + expression: Schema.String.annotate({ description: 'The LaTeX expression for the equation.', examples: ['E = mc^2'], }), }), annotations: TextAnnotations, - plain_text: Schema.String.annotations({ + plain_text: Schema.String.annotate({ description: 'Plain text representation of the equation.', }), - href: Schema.NullOr(Schema.String).annotations({ + href: Schema.NullOr(Schema.String).annotate({ description: 'Always null for equations.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.EquationRichText', title: 'Equation Rich Text', description: 'Rich text content containing a LaTeX equation.', @@ -374,7 +374,7 @@ export type EquationRichText = typeof EquationRichText.Type * * @see https://developers.notion.com/reference/rich-text */ -export const RichText = Schema.Union(TextRichText, MentionRichText, EquationRichText).annotations({ +export const RichText = Schema.Union([TextRichText, MentionRichText, EquationRichText]).annotate({ identifier: 'Notion.RichText', title: 'Rich Text', description: 'Rich text content supporting text, mentions, and equations.', @@ -386,7 +386,7 @@ export type RichText = typeof RichText.Type /** * Array of rich text elements. */ -export const RichTextArray = Schema.Array(RichText).annotations({ +export const RichTextArray = Schema.Array(RichText).annotate({ identifier: 'Notion.RichTextArray', title: 'Rich Text Array', description: 'An array of rich text elements.', @@ -420,7 +420,7 @@ export const RichTextArrayAsString = Schema.transform(RichTextArray, Schema.Stri href: null, }, ], -}).annotations({ +}).annotate({ identifier: 'Notion.RichTextArrayAsString', title: 'Rich Text as String', description: 'Transform rich text array to/from plain string.', diff --git a/packages/@overeng/notion-effect-schema/src/users.ts b/packages/@overeng/notion-effect-schema/src/users.ts index a40fb3dc68..bfa09d3012 100644 --- a/packages/@overeng/notion-effect-schema/src/users.ts +++ b/packages/@overeng/notion-effect-schema/src/users.ts @@ -12,15 +12,14 @@ import { docsPath, NotionUUID } from './common.ts' * @see https://developers.notion.com/reference/user#people */ export const PersonData = Schema.Struct({ - email: Schema.optionalWith( - Schema.String.annotations({ + email: Schema.OptionFromOptional( + Schema.String.annotate({ examples: ['user@example.com'], }), - { as: 'Option' }, - ).annotations({ + ).annotate({ description: 'Email address of the person. Only present with proper capabilities.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.PersonData', title: 'Person Data', description: 'Person-specific properties within a user object.', @@ -35,33 +34,31 @@ export type PersonData = typeof PersonData.Type * @see https://developers.notion.com/reference/user#people */ export const Person = Schema.Struct({ - object: Schema.Literal('user').annotations({ + object: Schema.Literal('user').annotate({ description: 'Always "user" for user objects.', }), - id: NotionUUID.annotations({ + id: NotionUUID.annotate({ description: 'Unique identifier for this user.', }), - type: Schema.Literal('person').annotations({ + type: Schema.Literal('person').annotate({ description: 'Type identifier for person users.', }), - name: Schema.optionalWith( - Schema.String.annotations({ + name: Schema.OptionFromOptional( + Schema.String.annotate({ examples: ['Jane Doe'], }), - { as: 'Option' }, - ).annotations({ + ).annotate({ description: "User's name as displayed in Notion.", }), - avatar_url: Schema.optionalWith( - Schema.String.annotations({ + avatar_url: Schema.OptionFromOptional( + Schema.String.annotate({ examples: ['https://s3.us-west-2.amazonaws.com/...'], }), - { as: 'Option' }, - ).annotations({ + ).annotate({ description: "URL of the user's avatar image.", }), person: PersonData, -}).annotations({ +}).annotate({ identifier: 'Notion.Person', title: 'Person', description: 'A human user in Notion.', @@ -77,7 +74,7 @@ export type Person = typeof Person.Type /** * Bot owner information. */ -export const BotOwner = Schema.Union( +export const BotOwner = Schema.Union([ Schema.Struct({ type: Schema.Literal('workspace'), workspace: Schema.Literal(true), @@ -89,7 +86,7 @@ export const BotOwner = Schema.Union( id: NotionUUID, }), }), -).annotations({ +]).annotate({ identifier: 'Notion.BotOwner', title: 'Bot Owner', description: 'The owner of a bot, either a workspace or a user.', @@ -104,15 +101,13 @@ export type BotOwner = typeof BotOwner.Type * @see https://developers.notion.com/reference/user#bots */ export const BotData = Schema.Struct({ - owner: BotOwner.annotations({ + owner: BotOwner.annotate({ description: 'Owner of the bot (workspace or user).', }), - workspace_name: Schema.optionalWith(Schema.String, { - as: 'Option', - }).annotations({ + workspace_name: Schema.OptionFromOptional(Schema.String).annotate({ description: 'Name of the workspace owning the bot.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.BotData', title: 'Bot Data', description: 'Bot-specific properties within a user object.', @@ -127,28 +122,27 @@ export type BotData = typeof BotData.Type * @see https://developers.notion.com/reference/user#bots */ export const Bot = Schema.Struct({ - object: Schema.Literal('user').annotations({ + object: Schema.Literal('user').annotate({ description: 'Always "user" for user objects.', }), - id: NotionUUID.annotations({ + id: NotionUUID.annotate({ description: 'Unique identifier for this bot.', }), - type: Schema.Literal('bot').annotations({ + type: Schema.Literal('bot').annotate({ description: 'Type identifier for bot users.', }), - name: Schema.optionalWith( - Schema.String.annotations({ + name: Schema.OptionFromOptional( + Schema.String.annotate({ examples: ['My Integration'], }), - { as: 'Option' }, - ).annotations({ + ).annotate({ description: "Bot's name as displayed in Notion.", }), - avatar_url: Schema.optionalWith(Schema.String, { as: 'Option' }).annotations({ + avatar_url: Schema.OptionFromOptional(Schema.String).annotate({ description: "URL of the bot's avatar image.", }), bot: BotData, -}).annotations({ +}).annotate({ identifier: 'Notion.Bot', title: 'Bot', description: 'A bot user (integration) in Notion.', @@ -166,13 +160,13 @@ export type Bot = typeof Bot.Type * Contains only the id and object type. */ export const PartialUser = Schema.Struct({ - object: Schema.Literal('user').annotations({ + object: Schema.Literal('user').annotate({ description: 'Always "user" for user objects.', }), - id: NotionUUID.annotations({ + id: NotionUUID.annotate({ description: 'Unique identifier for this user.', }), -}).annotations({ +}).annotate({ identifier: 'Notion.PartialUser', title: 'Partial User', description: 'A minimal user reference containing only the ID.', @@ -190,7 +184,7 @@ export type PartialUser = typeof PartialUser.Type * * @see https://developers.notion.com/reference/user */ -export const User = Schema.Union(Person, Bot).annotations({ +export const User = Schema.Union([Person, Bot]).annotate({ identifier: 'Notion.User', title: 'User', description: 'A Notion user, which can be either a person or a bot.', diff --git a/packages/@overeng/notion-effect-schema/src/webhook.ts b/packages/@overeng/notion-effect-schema/src/webhook.ts index 98d7b61903..d92ec36933 100644 --- a/packages/@overeng/notion-effect-schema/src/webhook.ts +++ b/packages/@overeng/notion-effect-schema/src/webhook.ts @@ -1,6 +1,6 @@ import { Schema } from 'effect' -const NonEmptyWebhookString = Schema.NonEmptyTrimmedString.annotations({ +const NonEmptyWebhookString = Schema.Trimmed.check(Schema.isNonEmpty()).annotate({ identifier: 'NotionWebhook.NonEmptyString', }) @@ -8,7 +8,7 @@ const NonEmptyWebhookString = Schema.NonEmptyTrimmedString.annotations({ export const NotionWebhookEntity = Schema.Struct({ id: NonEmptyWebhookString, type: NonEmptyWebhookString, -}).annotations({ identifier: 'NotionWebhook.Entity' }) +}).annotate({ identifier: 'NotionWebhook.Entity' }) export type NotionWebhookEntity = typeof NotionWebhookEntity.Type @@ -17,14 +17,14 @@ export const NotionWebhookParent = Schema.Struct({ page_id: Schema.optional(NonEmptyWebhookString), data_source_id: Schema.optional(NonEmptyWebhookString), database_id: Schema.optional(NonEmptyWebhookString), -}).annotations({ identifier: 'NotionWebhook.Parent' }) +}).annotate({ identifier: 'NotionWebhook.Parent' }) export type NotionWebhookParent = typeof NotionWebhookParent.Type /** `data` envelope nested on a Notion webhook event. */ export const NotionWebhookData = Schema.Struct({ parent: Schema.optional(NotionWebhookParent), -}).annotations({ identifier: 'NotionWebhook.Data' }) +}).annotate({ identifier: 'NotionWebhook.Data' }) export type NotionWebhookData = typeof NotionWebhookData.Type @@ -41,14 +41,14 @@ export const NotionWebhookPayload = Schema.Struct({ timestamp: Schema.optional(NonEmptyWebhookString), created_time: Schema.optional(NonEmptyWebhookString), api_version: Schema.optional(NonEmptyWebhookString), - attempt_number: Schema.optional(Schema.NonNegativeInt), + attempt_number: Schema.optional(Schema.Natural), subscription_id: Schema.optional(NonEmptyWebhookString), workspace_id: Schema.optional(NonEmptyWebhookString), integration_id: Schema.optional(NonEmptyWebhookString), is_aggregated: Schema.optional(Schema.Boolean), entity: Schema.optional(NotionWebhookEntity), data: Schema.optional(NotionWebhookData), -}).annotations({ identifier: 'NotionWebhook.Payload' }) +}).annotate({ identifier: 'NotionWebhook.Payload' }) export type NotionWebhookPayload = typeof NotionWebhookPayload.Type diff --git a/packages/@overeng/notion-md/nix/build.nix b/packages/@overeng/notion-md/nix/build.nix index d12229aa47..2bef4cf449 100644 --- a/packages/@overeng/notion-md/nix/build.nix +++ b/packages/@overeng/notion-md/nix/build.nix @@ -20,7 +20,7 @@ let workspaceRoot = src; # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { - "." = mkSharedHash "sha256-LAC9pK+sG1r5o5fUea8XNQexsl4gda0JpVzOYndD5EU="; + "." = mkSharedHash "sha256-KRvcFI2H/W7pTs2wSy7W5vNMQbUo0dsvFZY48tTIN3I="; }; smokeTestArgs = [ "--help" ]; inherit gitRev commitTs dirty; diff --git a/packages/@overeng/notion-md/package.json b/packages/@overeng/notion-md/package.json index 6eec7f311b..e967e063c1 100644 --- a/packages/@overeng/notion-md/package.json +++ b/packages/@overeng/notion-md/package.json @@ -34,16 +34,9 @@ "@overeng/utils": "workspace:^" }, "devDependencies": { - "@effect-atom/atom": "0.5.3", - "@effect/cli": "0.75.2", - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@overeng/tui-react": "workspace:^", "@overeng/utils-dev": "workspace:^", "@playwright/test": "1.61.0", @@ -53,7 +46,7 @@ "@types/react": "19.2.17", "@types/react-reconciler": "0.33.0", "@vitejs/plugin-react": "6.0.2", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-dom": "19.2.7", "react-reconciler": "0.33.0", @@ -63,16 +56,10 @@ "vitest": "4.1.9" }, "peerDependencies": { - "@effect/cli": "^0.75.2", - "@effect/cluster": "^0.59.0", - "@effect/experimental": "^0.60.0", - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/rpc": "^0.75.1", - "@effect/workflow": "^0.18.2", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@playwright/test": "^1.61.0", - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/notion-md/package.json.genie.ts b/packages/@overeng/notion-md/package.json.genie.ts index 8a18b58061..65de5a8fd2 100644 --- a/packages/@overeng/notion-md/package.json.genie.ts +++ b/packages/@overeng/notion-md/package.json.genie.ts @@ -17,18 +17,7 @@ import tuiReactPkg from '../tui-react/package.json.genie.ts' import utilsDevPkg from '../utils-dev/package.json.genie.ts' import utilsPkg from '../utils/package.json.genie.ts' -const peerDepNames = [ - '@effect/cli', - '@effect/cluster', - '@effect/experimental', - '@effect/opentelemetry', - '@effect/platform', - '@effect/platform-node', - '@effect/rpc', - '@effect/workflow', - '@playwright/test', - 'effect', -] as const +const peerDepNames = ['effect', '@effect/opentelemetry', '@effect/platform-node', '@playwright/test'] as const const workspaceDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/notion-md' }), @@ -46,24 +35,7 @@ const workspaceDeps = catalog.compose({ devDependencies: { workspace: [tuiReactPkg, utilsDevPkg], external: { - ...catalog.pick( - ...peerDepNames, - '@effect-atom/atom', - '@effect/vitest', - '@storybook/react', - '@storybook/react-vite', - '@types/node', - '@types/react', - '@types/react-reconciler', - '@vitejs/plugin-react', - 'react', - 'react-dom', - 'react-reconciler', - 'storybook', - 'typescript', - 'vite', - 'vitest', - ), + ...catalog.pick(...peerDepNames, '@effect/vitest', '@storybook/react', '@storybook/react-vite', '@types/node', '@types/react', '@types/react-reconciler', '@vitejs/plugin-react', 'react', 'react-dom', 'react-reconciler', 'storybook', 'typescript', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/notion-md/src/batch.ts b/packages/@overeng/notion-md/src/batch.ts index 0dfd07458e..d49b5f6cea 100644 --- a/packages/@overeng/notion-md/src/batch.ts +++ b/packages/@overeng/notion-md/src/batch.ts @@ -429,7 +429,7 @@ const coalesceTriggers = (triggers: Iterable): readonly WatchTrigg .toSorted((left, right) => left.path.localeCompare(right.path)) } -const encodeJsonLine = Schema.encodeSync(Schema.parseJson()) +const encodeJsonLine = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)) const writeJsonLine = (value: unknown): Effect.Effect => Effect.sync(() => { diff --git a/packages/@overeng/notion-md/src/body-facade.ts b/packages/@overeng/notion-md/src/body-facade.ts index 6525789cc1..c476422283 100644 --- a/packages/@overeng/notion-md/src/body-facade.ts +++ b/packages/@overeng/notion-md/src/body-facade.ts @@ -1,5 +1,5 @@ -import type { FileSystem } from '@effect/platform' import { Effect, Schema } from 'effect' +import type { FileSystem } from 'effect/FileSystem' import { descriptorForUtf8, type ContentDescriptor } from '@overeng/content-address' import { describeBodyLossyRefusal, type BodyCompleteness } from '@overeng/notion-core' @@ -20,10 +20,10 @@ import { trackPage, type TrackResult } from './reconcile.ts' import { NmdStateStore } from './state-store.ts' /** Raised when the body-only facade refuses a stale verified operation. */ -export class NotionMdBodyConflictError extends Schema.TaggedError()( +export class NotionMdBodyConflictError extends Schema.TaggedErrorClass()( 'NotionMdBodyConflictError', { - operation: Schema.Literal('replace_remote_body_verified', 'settle_verified_body_push'), + operation: Schema.Literals(['replace_remote_body_verified', 'settle_verified_body_push']), page_id: Schema.String, path: Schema.optional(Schema.String), expected_body_hash: Schema.String, diff --git a/packages/@overeng/notion-md/src/body-facade.unit.test.ts b/packages/@overeng/notion-md/src/body-facade.unit.test.ts index b1af5740a7..b787698909 100644 --- a/packages/@overeng/notion-md/src/body-facade.unit.test.ts +++ b/packages/@overeng/notion-md/src/body-facade.unit.test.ts @@ -1,5 +1,5 @@ -import type { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' +import type { FileSystem } from 'effect/FileSystem' import { Effect, Layer, Schema } from 'effect' import { describe, expect, it } from 'vitest' @@ -32,7 +32,7 @@ import { NmdStateStore, type NmdStateStoreShape } from './state-store.ts' const pageId = '00000000-0000-4000-8000-000000000001' const path = '/tmp/page.nmd' -const encodeJson = Schema.encodeSync(Schema.parseJson()) +const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)) const emptyStorage = (): NmdStorage => ({ _tag: 'self_contained', @@ -210,7 +210,7 @@ const runWithGatewayAndStore = ( store: FakeStore, ) => Effect.runPromise( - effect.pipe(Effect.provide(Layer.mergeAll(gateway.layer, store.layer, NodeContext.layer))), + effect.pipe(Effect.provide(Layer.mergeAll(gateway.layer, store.layer, NodeServices.layer))), ) describe('notion-md body facade', () => { diff --git a/packages/@overeng/notion-md/src/canonical-markdown.test.ts b/packages/@overeng/notion-md/src/canonical-markdown.test.ts index 0c0ba812bb..0023139785 100644 --- a/packages/@overeng/notion-md/src/canonical-markdown.test.ts +++ b/packages/@overeng/notion-md/src/canonical-markdown.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { describe, expect, it } from '@effect/vitest' -import * as fc from 'effect/FastCheck' +import * as fc from 'effect/testing/FastCheck' import { canonicalizeBlockMarkdown, semanticEquivalent } from './canonical-markdown.ts' import { sha256Digest } from './hash.ts' diff --git a/packages/@overeng/notion-md/src/cli-program.ts b/packages/@overeng/notion-md/src/cli-program.ts index 16081d902a..e7b5121700 100644 --- a/packages/@overeng/notion-md/src/cli-program.ts +++ b/packages/@overeng/notion-md/src/cli-program.ts @@ -1,8 +1,8 @@ import { basename, dirname, resolve } from 'node:path' -import { Args, Command, Options } from '@effect/cli' import { FetchHttpClient, FileSystem, Path } from '@effect/platform' import { Cause, Console, Duration, Effect, Layer, Option, Queue, Schema, Stream } from 'effect' +import { Argument as Args, Command, Flag as Options } from 'effect/unstable/cli' import { NMD_SYNC_DIRECTORY, @@ -55,11 +55,11 @@ import { import type { SyncOptions } from './sync.ts' import { NOTION_MD_VERSION } from './version.ts' -const NonEmptyCliText = Schema.NonEmptyTrimmedString.annotations({ +const NonEmptyCliText = Schema.Trimmed.check(Schema.isNonEmpty()).annotate({ identifier: 'NotionMd.Cli.NonEmptyText', }) -const PositiveInteger = Schema.Number.pipe(Schema.int(), Schema.positive()).annotations({ +const PositiveInteger = Schema.Number.pipe(Schema.int(), Schema.positive()).annotate({ identifier: 'NotionMd.Cli.PositiveInteger', }) @@ -71,29 +71,29 @@ const PositiveInteger = Schema.Number.pipe(Schema.int(), Schema.positive()).anno */ /** Local `.nmd` paths (file or directory). `status`/`sync` take only local paths. */ -const localTargetsArg = Args.text({ name: 'path' }).pipe( +const localTargetsArg = Args.string('path').pipe( Args.withDescription('Local .nmd file or directory (a directory means everything under it)'), Args.withSchema(NonEmptyCliText), Args.atLeast(1), ) /** `track` is the only command that takes a Notion page id/url. */ -const trackPageRefArg = Args.text({ name: 'page-id-or-url' }).pipe( +const trackPageRefArg = Args.string('page-id-or-url').pipe( Args.withDescription('Notion page id or URL to track'), Args.withSchema(NonEmptyCliText), ) -const trackOutPathArg = Args.text({ name: 'path' }).pipe( +const trackOutPathArg = Args.string('path').pipe( Args.withDescription('Local .nmd file to write (default: .nmd)'), Args.withSchema(NonEmptyCliText), Args.optional, ) -const SourceLiteral = Schema.Literal('local', 'remote', 'shared').annotations({ +const SourceLiteral = Schema.Literals(['local', 'remote', 'shared']).annotate({ identifier: 'NotionMd.Cli.Source', }) -const trackAsOption = Options.text('as').pipe( +const trackAsOption = Options.string('as').pipe( Options.withDescription( 'Sync direction to record (local|remote|shared); default remote — this tracks existing Notion state', ), @@ -683,7 +683,7 @@ export const readAllSyncStates = ( ) const strictOptions = { errors: 'all', onExcessProperty: 'error' } as const const decodeSyncState = Schema.decodeUnknown( - Schema.parseJson(NmdSyncStateV1Schema), + Schema.fromJsonString(NmdSyncStateV1Schema), strictOptions, ) const syncStates: NmdSyncStateV1[] = [] @@ -875,7 +875,7 @@ const gcCommand = Command.make( // Editor surfaces: cat / put / edit (VRS "Editor Surfaces") // --------------------------------------------------------------------------- -const pageArg = Args.text({ name: 'page' }).pipe( +const pageArg = Args.string('page').pipe( Args.withDescription('Notion page id, dashed id, or URL'), Args.withSchema(NonEmptyCliText), ) @@ -887,7 +887,7 @@ const frontmatterOption = Options.boolean('frontmatter').pipe( Options.withDefault(false), ) -const baseHashOption = Options.text('base-hash').pipe( +const baseHashOption = Options.string('base-hash').pipe( Options.withDescription('Optimistic-concurrency token from a prior `cat` (guards the write)'), Options.optional, ) @@ -1071,7 +1071,7 @@ export const cli = Command.run(notionMdCommand, { export const renderCliError = (cause: Cause.Cause) => Cause.isInterruptedOnly(cause) === true ? Effect.void - : Option.match(Cause.failureOption(cause), { + : Option.match(Cause.findErrorOption(cause), { onNone: () => Effect.logError(cause), onSome: (error) => Effect.logError(error), }) diff --git a/packages/@overeng/notion-md/src/cli.e2e.test.ts b/packages/@overeng/notion-md/src/cli.e2e.test.ts index c49a0703ef..e2d1ff0253 100644 --- a/packages/@overeng/notion-md/src/cli.e2e.test.ts +++ b/packages/@overeng/notion-md/src/cli.e2e.test.ts @@ -5,7 +5,7 @@ import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { promisify } from 'node:util' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer } from 'effect' import { describe, expect, it } from 'vitest' @@ -45,12 +45,12 @@ const runCli = (args: readonly string[]) => }, }) -const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)) +const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)) const runStore = ( effect: Effect.Effect, ): Promise => - Effect.runPromise(effect.pipe(Effect.provide(Layer.mergeAll(stateStoreLayer, NodeContext.layer)))) + Effect.runPromise(effect.pipe(Effect.provide(Layer.mergeAll(stateStoreLayer, NodeServices.layer)))) const syncStateFor = (opts: { readonly pageId: string diff --git a/packages/@overeng/notion-md/src/cli.ts b/packages/@overeng/notion-md/src/cli.ts index 52dfebb129..ff458cac5c 100644 --- a/packages/@overeng/notion-md/src/cli.ts +++ b/packages/@overeng/notion-md/src/cli.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect, type Exit, Layer, Schema } from 'effect' import { ServiceIdentity } from '@overeng/otel-contract' @@ -22,7 +22,7 @@ export const runCliMain = ({ Effect.tapErrorCause(renderCliError), Effect.scoped, Effect.provide( - Layer.mergeAll(NodeContext.layer, withTelemetry({ identity, shape: 'cli', endpoint })), + Layer.mergeAll(NodeServices.layer, withTelemetry({ identity, shape: 'cli', endpoint })), ), ) }) diff --git a/packages/@overeng/notion-md/src/corpus-live.integration.test.ts b/packages/@overeng/notion-md/src/corpus-live.integration.test.ts index 3d9261c1a6..b7d7a52aed 100644 --- a/packages/@overeng/notion-md/src/corpus-live.integration.test.ts +++ b/packages/@overeng/notion-md/src/corpus-live.integration.test.ts @@ -1,8 +1,8 @@ import { writeFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' -import { FetchHttpClient, type HttpClient } from '@effect/platform' import { Effect, Layer, Redacted } from 'effect' +import { FetchHttpClient, type HttpClient } from 'effect/unstable/http' import { afterAll, describe, expect, it } from 'vitest' import { diff --git a/packages/@overeng/notion-md/src/corpus.ts b/packages/@overeng/notion-md/src/corpus.ts index 7d597492c6..53af89e2e7 100644 --- a/packages/@overeng/notion-md/src/corpus.ts +++ b/packages/@overeng/notion-md/src/corpus.ts @@ -18,13 +18,13 @@ import { fidelityCorpusData } from './corpus/fidelity-corpus.ts' */ /** R33 relation an entry asserts against its own round-trip or a sibling. */ -export const CorpusRelation = Schema.Literal('equal', 'distinct_from').annotations({ +export const CorpusRelation = Schema.Literals(['equal', 'distinct_from']).annotate({ identifier: 'NotionMd.Corpus.Relation', }) /** One historically-broken Notion body shape and the relation it must hold. */ export const CorpusEntry = Schema.Struct({ - id: Schema.NonEmptyTrimmedString, + id: Schema.Trimmed.check(Schema.isNonEmpty()), issue: Schema.String, description: Schema.String, /** What a user authors locally. */ @@ -34,7 +34,7 @@ export const CorpusEntry = Schema.Struct({ relation: CorpusRelation, /** For `distinct_from`, the sibling entry id whose canonical form must differ. */ distinct_from: Schema.optional(Schema.String), -}).annotations({ identifier: 'NotionMd.Corpus.Entry' }) +}).annotate({ identifier: 'NotionMd.Corpus.Entry' }) export type CorpusEntry = typeof CorpusEntry.Type @@ -42,7 +42,7 @@ export type CorpusEntry = typeof CorpusEntry.Type export const Corpus = Schema.Struct({ captured: Schema.String, entries: Schema.Array(CorpusEntry), -}).annotations({ identifier: 'NotionMd.Corpus' }) +}).annotate({ identifier: 'NotionMd.Corpus' }) export type Corpus = typeof Corpus.Type diff --git a/packages/@overeng/notion-md/src/editor-commands.ts b/packages/@overeng/notion-md/src/editor-commands.ts index fb981448a6..6740c5d1a7 100644 --- a/packages/@overeng/notion-md/src/editor-commands.ts +++ b/packages/@overeng/notion-md/src/editor-commands.ts @@ -1,6 +1,7 @@ import { join } from 'node:path' -import { Command as PlatformCommand, type CommandExecutor, FileSystem } from '@effect/platform' +import { ChildProcess as PlatformCommand, type ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' import { Console, Effect } from 'effect' import { describeBodyLossyRefusal } from '@overeng/notion-core' diff --git a/packages/@overeng/notion-md/src/editor-edit.e2e.test.ts b/packages/@overeng/notion-md/src/editor-edit.e2e.test.ts index 4e24962f1a..c6e162c69f 100644 --- a/packages/@overeng/notion-md/src/editor-edit.e2e.test.ts +++ b/packages/@overeng/notion-md/src/editor-edit.e2e.test.ts @@ -2,8 +2,8 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer } from 'effect' import { describe, expect, it } from 'vitest' @@ -22,14 +22,14 @@ import { normalizeMarkdownLineEndings } from './hash.ts' import { NotionMdGateway, type NotionMdGatewayShape } from './model.ts' import { NmdStateStoreLive, type NmdStateStore } from './state-store.ts' -const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)) +const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)) const runEdit = ( effect: Effect.Effect, gateway: FakeGateway, ) => Effect.either(effect).pipe( - Effect.provide(Layer.mergeAll(gateway.layer, stateStoreLayer, NodeContext.layer)), + Effect.provide(Layer.mergeAll(gateway.layer, stateStoreLayer, NodeServices.layer)), Effect.runPromise, ) @@ -232,7 +232,7 @@ const runReadOnly = ( ) => Effect.either(effect).pipe( // Deliberately NO stateStoreLayer: read-only's narrow R never needs it. - Effect.provide(Layer.mergeAll(gateway.layer, NodeContext.layer)), + Effect.provide(Layer.mergeAll(gateway.layer, NodeServices.layer)), Effect.runPromise, ) diff --git a/packages/@overeng/notion-md/src/editor-observability.unit.test.ts b/packages/@overeng/notion-md/src/editor-observability.unit.test.ts index d0578ddd37..329bd35009 100644 --- a/packages/@overeng/notion-md/src/editor-observability.unit.test.ts +++ b/packages/@overeng/notion-md/src/editor-observability.unit.test.ts @@ -25,8 +25,8 @@ * that would assert non-existent attributes. */ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { FileSystem } from 'effect/FileSystem' +import { NodeServices } from '@effect/platform-node' import { expect, layer } from '@effect/vitest' import { Effect, Layer } from 'effect' @@ -162,7 +162,7 @@ const scriptedEditor = ), ) -const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)) +const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)) /** R24 leak guard applied across every captured span attribute value. */ const assertNoSensitiveAttrs = ( @@ -250,7 +250,7 @@ layer(CaptureLayer, { excludeTestServices: true })('editor span shapes (Group G) mode: 'default', pageRef: pageId, runEditor: scriptedEditor((buffer) => buffer.replace('original line', SENTINEL_BODY)), - }).pipe(Effect.provide(Layer.mergeAll(gateway.layer, stateStoreLayer, NodeContext.layer))) + }).pipe(Effect.provide(Layer.mergeAll(gateway.layer, stateStoreLayer, NodeServices.layer))) expect(result.outcome).toBe('pushed') yield* flushCaptureSpans({ exportInterval }) @@ -290,7 +290,7 @@ layer(CaptureLayer, { excludeTestServices: true })('editor span shapes (Group G) writeStderr: () => Effect.void, // The editor "edits" the buffer, but read-only discards it. runEditor: scriptedEditor((buffer) => `${buffer}\ndiscarded edit`), - }).pipe(Effect.provide(Layer.mergeAll(gateway.layer, stateStoreLayer, NodeContext.layer))) + }).pipe(Effect.provide(Layer.mergeAll(gateway.layer, stateStoreLayer, NodeServices.layer))) expect(result.outcome).toBe('read-only') // Read-only never calls updateMarkdown: the remote body is untouched. expect(gateway.state.body).toBe(normalizeMarkdownLineEndings(SENTINEL_BODY)) diff --git a/packages/@overeng/notion-md/src/editor-test-harness.ts b/packages/@overeng/notion-md/src/editor-test-harness.ts index 1b85f1d18d..2fb51c5da5 100644 --- a/packages/@overeng/notion-md/src/editor-test-harness.ts +++ b/packages/@overeng/notion-md/src/editor-test-harness.ts @@ -4,7 +4,7 @@ * the engine through one fake instead of duplicating ~100 lines. Lives in a * non-`.test.ts` module so vitest does not collect it as a suite. */ -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect, Layer } from 'effect' import type { BodyCompleteness } from '@overeng/notion-core' diff --git a/packages/@overeng/notion-md/src/errors.ts b/packages/@overeng/notion-md/src/errors.ts index 873f458ed5..865bafa089 100644 --- a/packages/@overeng/notion-md/src/errors.ts +++ b/packages/@overeng/notion-md/src/errors.ts @@ -5,61 +5,64 @@ import { PropertyWriteGuardName } from '@overeng/notion-property-write' import { DestructiveBodyGuardName, NonBodyGuardName } from './non-body-guards.ts' /** Raised when a local `.nmd` file is missing or has malformed frontmatter. */ -export class NmdFrontmatterError extends Schema.TaggedError()( +export class NmdFrontmatterError extends Schema.TaggedErrorClass()( 'NmdFrontmatterError', { path: Schema.String, message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** Raised when `.nmd` frontmatter points at an invalid local object-store entry. */ -export class NmdObjectStoreError extends Schema.TaggedError()( +export class NmdObjectStoreError extends Schema.TaggedErrorClass()( 'NmdObjectStoreError', { path: Schema.String, object_path: Schema.String, message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** Raised when local and remote edits cannot be reconciled automatically. */ -export class NmdConflictError extends Schema.TaggedError()('NmdConflictError', { - path: Schema.String, - page_id: Schema.String, - message: Schema.String, - local_changed: Schema.Boolean, - remote_changed: Schema.Boolean, - conflict_path: Schema.optional(Schema.String), - cause: Schema.optional(Schema.Defect), -}) {} +export class NmdConflictError extends Schema.TaggedErrorClass()( + 'NmdConflictError', + { + path: Schema.String, + page_id: Schema.String, + message: Schema.String, + local_changed: Schema.Boolean, + remote_changed: Schema.Boolean, + conflict_path: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect()), + }, +) {} /** Raised when local filesystem IO fails while reading or writing sync state. */ -export class NmdFileSystemError extends Schema.TaggedError()( +export class NmdFileSystemError extends Schema.TaggedErrorClass()( 'NmdFileSystemError', { operation: Schema.String, path: Schema.String, message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** Raised when a Notion gateway operation fails at the API boundary. */ -export class NmdGatewayError extends Schema.TaggedError()('NmdGatewayError', { +export class NmdGatewayError extends Schema.TaggedErrorClass()('NmdGatewayError', { operation: Schema.String, page_id: Schema.optional(Schema.String), block_id: Schema.optional(Schema.String), /** Log-safe fingerprint of the active Notion integration token (see `notionTokenFingerprint`). */ token_fingerprint: Schema.optional(Schema.String), message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }) {} /** Raised when remote Markdown cannot safely become a clean body base. */ -export class NmdRemoteBodyLossyError extends Schema.TaggedError()( +export class NmdRemoteBodyLossyError extends Schema.TaggedErrorClass()( 'NmdRemoteBodyLossyError', { operation: Schema.String, @@ -76,7 +79,7 @@ export class NmdRemoteBodyLossyError extends Schema.TaggedError()( +export class NmdSchemaDriftError extends Schema.TaggedErrorClass()( 'NmdSchemaDriftError', { page_id: Schema.String, @@ -95,7 +98,7 @@ export class NmdSchemaDriftError extends Schema.TaggedError * local property mutation: the standalone proof provider refuses to mint a * proof, since a local write against a Notion-authoritative page is drift. */ -export class NmdPropertyWriteBlockedError extends Schema.TaggedError()( +export class NmdPropertyWriteBlockedError extends Schema.TaggedErrorClass()( 'NmdPropertyWriteBlockedError', { page_id: Schema.String, @@ -116,7 +119,7 @@ export class NmdPropertyWriteBlockedError extends Schema.TaggedError()( +export class NmdNonBodyWriteBlockedError extends Schema.TaggedErrorClass()( 'NmdNonBodyWriteBlockedError', { page_id: Schema.String, @@ -144,7 +147,7 @@ export class NmdNonBodyWriteBlockedError extends Schema.TaggedError()( +export class NmdDestructiveBodyBlockedError extends Schema.TaggedErrorClass()( 'NmdDestructiveBodyBlockedError', { page_id: Schema.String, @@ -157,7 +160,7 @@ export class NmdDestructiveBodyBlockedError extends Schema.TaggedError()( +export class NmdTokenMissingError extends Schema.TaggedErrorClass()( 'NmdTokenMissingError', { message: Schema.String, @@ -168,12 +171,12 @@ export class NmdTokenMissingError extends Schema.TaggedError` is not a valid Notion id/URL, or the page does not exist * (editor surfaces `cat`/`put`/`edit`; exit 4). */ -export class NmdUnresolvablePageError extends Schema.TaggedError()( +export class NmdUnresolvablePageError extends Schema.TaggedErrorClass()( 'NmdUnresolvablePageError', { page: Schema.String, message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} @@ -181,7 +184,7 @@ export class NmdUnresolvablePageError extends Schema.TaggedError()( +export class NmdInvalidDocumentError extends Schema.TaggedErrorClass()( 'NmdInvalidDocumentError', { page_id: Schema.optional(Schema.String), @@ -190,7 +193,7 @@ export class NmdInvalidDocumentError extends Schema.TaggedError()( +export class NmdEditorAbortedError extends Schema.TaggedErrorClass()( 'NmdEditorAbortedError', { page_id: Schema.String, @@ -204,7 +207,7 @@ export class NmdEditorAbortedError extends Schema.TaggedError()( +export class NmdPostPushGateError extends Schema.TaggedErrorClass()( 'NmdPostPushGateError', { page_id: Schema.String, @@ -216,19 +219,19 @@ export class NmdPostPushGateError extends Schema.TaggedError()( +export class NmdPartialWriteError extends Schema.TaggedErrorClass()( 'NmdPartialWriteError', { page_id: Schema.String, body_written: Schema.Boolean, title_written: Schema.Boolean, message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} /** Raised for invalid command-line arguments. */ -export class NmdCliError extends Schema.TaggedError()('NmdCliError', { +export class NmdCliError extends Schema.TaggedErrorClass()('NmdCliError', { message: Schema.String, }) {} diff --git a/packages/@overeng/notion-md/src/frontmatter.ts b/packages/@overeng/notion-md/src/frontmatter.ts index 4801ff8136..7511252b57 100644 --- a/packages/@overeng/notion-md/src/frontmatter.ts +++ b/packages/@overeng/notion-md/src/frontmatter.ts @@ -13,12 +13,15 @@ export interface ParsedNmdFile { } const frontmatterEndMarker = '\n---\n' -const decodeNmdFrontmatterJson = Schema.decodeUnknown(Schema.parseJson(NmdFrontmatterV2Schema), { - errors: 'all', - onExcessProperty: 'error', -}) +const decodeNmdFrontmatterJson = Schema.decodeUnknown( + Schema.fromJsonString(NmdFrontmatterV2Schema), + { + errors: 'all', + onExcessProperty: 'error', + }, +) const encodeNmdFrontmatterJsonSync = Schema.encodeSync( - Schema.parseJson(NmdFrontmatterV2Schema, { space: 2 }), + Schema.fromJsonString(NmdFrontmatterV2Schema, { space: 2 }), ) /** Render strict frontmatter as JSON-compatible YAML to keep parsing dependency-free. */ diff --git a/packages/@overeng/notion-md/src/gateway-token-fingerprint.unit.test.ts b/packages/@overeng/notion-md/src/gateway-token-fingerprint.unit.test.ts index ec937091d8..0ba93dccca 100644 --- a/packages/@overeng/notion-md/src/gateway-token-fingerprint.unit.test.ts +++ b/packages/@overeng/notion-md/src/gateway-token-fingerprint.unit.test.ts @@ -1,5 +1,5 @@ -import { HttpClient, HttpClientResponse } from '@effect/platform' import { Effect, Exit, Layer, Redacted } from 'effect' +import { HttpClient, HttpClientResponse } from 'effect/unstable/http' import { describe, expect, it } from 'vitest' import { NotionConfig, notionTokenFingerprint } from '@overeng/notion-effect-client' diff --git a/packages/@overeng/notion-md/src/live.integration.test.ts b/packages/@overeng/notion-md/src/live.integration.test.ts index 20138e9a5c..28c40d37a4 100644 --- a/packages/@overeng/notion-md/src/live.integration.test.ts +++ b/packages/@overeng/notion-md/src/live.integration.test.ts @@ -2,10 +2,10 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { FileSystem } from '@effect/platform' -import { FetchHttpClient, type HttpClient } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' +import type { FileSystem } from 'effect/FileSystem' import { Chunk, Effect, Layer, Redacted, Schema, Stream } from 'effect' +import { FetchHttpClient, type HttpClient } from 'effect/unstable/http' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { @@ -71,11 +71,11 @@ const ConfigLayer = NotionConfigLive({ }) const BaseLayer = Layer.mergeAll(ConfigLayer, FetchHttpClient.layer) -const StateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)) +const StateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)) const TestLayer = Layer.mergeAll( BaseLayer, StateStoreLayer, - NodeContext.layer, + NodeServices.layer, NotionMdGatewayLive.pipe(Layer.provide(BaseLayer)), ) @@ -98,7 +98,7 @@ const scratchTitlePrefix = 'notion-md e2e: ' const ledgerTitle = 'notion-md e2e run ledger' /** Raised when live cleanup targets a parent page outside the test allowlist. */ -class DisallowedTestParentPageError extends Schema.TaggedError()( +class DisallowedTestParentPageError extends Schema.TaggedErrorClass()( 'DisallowedTestParentPageError', { message: Schema.String, diff --git a/packages/@overeng/notion-md/src/live.ts b/packages/@overeng/notion-md/src/live.ts index a6485c4922..b599121378 100644 --- a/packages/@overeng/notion-md/src/live.ts +++ b/packages/@overeng/notion-md/src/live.ts @@ -1,5 +1,5 @@ -import { HttpClient } from '@effect/platform' import { Effect, Layer, Stream } from 'effect' +import { HttpClient } from 'effect/unstable/http' import { NotionBlocks, diff --git a/packages/@overeng/notion-md/src/merge.test.ts b/packages/@overeng/notion-md/src/merge.test.ts index 1604ee8033..ef9f9d0ccc 100644 --- a/packages/@overeng/notion-md/src/merge.test.ts +++ b/packages/@overeng/notion-md/src/merge.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@effect/vitest' -import * as fc from 'effect/FastCheck' +import * as fc from 'effect/testing/FastCheck' import { canonicalizeBlockMarkdown } from './canonical-markdown.ts' import { normalizeMarkdownLineEndings } from './hash.ts' diff --git a/packages/@overeng/notion-md/src/model.ts b/packages/@overeng/notion-md/src/model.ts index 304ed275bb..f379133ba6 100644 --- a/packages/@overeng/notion-md/src/model.ts +++ b/packages/@overeng/notion-md/src/model.ts @@ -193,7 +193,6 @@ export interface NotionMdGatewayShape { } /** Effect service tag for Notion Markdown sync operations. */ -export class NotionMdGateway extends Context.Tag('NotionMdGateway')< - NotionMdGateway, - NotionMdGatewayShape ->() {} +export class NotionMdGateway extends Context.Service()( + 'NotionMdGateway', +) {} diff --git a/packages/@overeng/notion-md/src/non-body-guards.ts b/packages/@overeng/notion-md/src/non-body-guards.ts index f3229bd401..24415b1341 100644 --- a/packages/@overeng/notion-md/src/non-body-guards.ts +++ b/packages/@overeng/notion-md/src/non-body-guards.ts @@ -38,7 +38,7 @@ export const nonBodyGuardNames = [ ] as const /** A single files/media or comment write guard name. */ -export const NonBodyGuardName = Schema.Literal(...nonBodyGuardNames).annotations({ +export const NonBodyGuardName = Schema.Literal(...nonBodyGuardNames).annotate({ identifier: 'NotionMd.NonBodyGuardName', }) export type NonBodyGuardName = typeof NonBodyGuardName.Type @@ -58,7 +58,7 @@ export const destructiveBodyGuardNames = [ ] as const /** A single destructive body write guard name. */ -export const DestructiveBodyGuardName = Schema.Literal(...destructiveBodyGuardNames).annotations({ +export const DestructiveBodyGuardName = Schema.Literal(...destructiveBodyGuardNames).annotate({ identifier: 'NotionMd.DestructiveBodyGuardName', }) export type DestructiveBodyGuardName = typeof DestructiveBodyGuardName.Type diff --git a/packages/@overeng/notion-md/src/path.ts b/packages/@overeng/notion-md/src/path.ts index 668519bc6c..f53fe28fc6 100644 --- a/packages/@overeng/notion-md/src/path.ts +++ b/packages/@overeng/notion-md/src/path.ts @@ -1,7 +1,7 @@ import { basename } from 'node:path' -import type { Path } from '@effect/platform' -import { FileSystem } from '@effect/platform' +import type { Path } from 'effect/Path' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import type { BatchResult } from './batch.ts' diff --git a/packages/@overeng/notion-md/src/progress.ts b/packages/@overeng/notion-md/src/progress.ts index 7d956ab061..02e7802145 100644 --- a/packages/@overeng/notion-md/src/progress.ts +++ b/packages/@overeng/notion-md/src/progress.ts @@ -31,10 +31,9 @@ export interface ProgressReporterShape { } /** Render seam for staged write-path sync progress (decision 0018, R43–R45). */ -export class ProgressReporter extends Context.Tag('ProgressReporter')< - ProgressReporter, - ProgressReporterShape ->() {} +export class ProgressReporter extends Context.Service()( + 'ProgressReporter', +) {} /** * Emit a reporter call without adding to the engine's `R`, and swallowing ALL diff --git a/packages/@overeng/notion-md/src/progress.unit.test.ts b/packages/@overeng/notion-md/src/progress.unit.test.ts index ada3164d4c..fde38faae1 100644 --- a/packages/@overeng/notion-md/src/progress.unit.test.ts +++ b/packages/@overeng/notion-md/src/progress.unit.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer } from 'effect' import { describe, expect, it } from 'vitest' @@ -12,7 +12,7 @@ import type { NotionMdGateway } from './model.ts' import { ProgressReporter, type ProgressReporterShape, type ProgressStage } from './progress.ts' import { NmdStateStoreLive, type NmdStateStore } from './state-store.ts' -const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)) +const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)) /** A captured progress event: the method that fired and the stage/note payload. */ type ProgressEvent = @@ -43,7 +43,7 @@ const runEdit = ( gateway: FakeGateway, progressLayer?: Layer.Layer, ) => { - const base = Layer.mergeAll(gateway.layer, stateStoreLayer, NodeContext.layer) + const base = Layer.mergeAll(gateway.layer, stateStoreLayer, NodeServices.layer) const layer = progressLayer === undefined ? base : Layer.merge(base, progressLayer) return Effect.either(effect).pipe( Effect.provide(layer as Layer.Layer), diff --git a/packages/@overeng/notion-md/src/reconcile-live.integration.test.ts b/packages/@overeng/notion-md/src/reconcile-live.integration.test.ts index 66a14ceaab..edafb03443 100644 --- a/packages/@overeng/notion-md/src/reconcile-live.integration.test.ts +++ b/packages/@overeng/notion-md/src/reconcile-live.integration.test.ts @@ -2,10 +2,11 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { FileSystem, HttpClient } from '@effect/platform' -import { FetchHttpClient } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' +import type { FileSystem } from 'effect/FileSystem' import { Deferred, Effect, Fiber, Layer, Redacted } from 'effect' +import type { HttpClient } from 'effect/unstable/http' +import { FetchHttpClient } from 'effect/unstable/http' import { afterAll, describe, expect, it } from 'vitest' import { NotionConfigLive, NotionPages, type NotionConfig } from '@overeng/notion-effect-client' @@ -48,8 +49,8 @@ const ConfigLayer = NotionConfigLive({ const BaseLayer = Layer.mergeAll(ConfigLayer, FetchHttpClient.layer) const TestLayer = Layer.mergeAll( BaseLayer, - NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)), - NodeContext.layer, + NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)), + NodeServices.layer, NotionMdGatewayLive.pipe(Layer.provide(BaseLayer)), ) diff --git a/packages/@overeng/notion-md/src/reconcile.e2e.test.ts b/packages/@overeng/notion-md/src/reconcile.e2e.test.ts index 752e66bcf4..d0516e8493 100644 --- a/packages/@overeng/notion-md/src/reconcile.e2e.test.ts +++ b/packages/@overeng/notion-md/src/reconcile.e2e.test.ts @@ -2,7 +2,7 @@ import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Cause, Effect, Exit, Layer, Option } from 'effect' import { describe, expect, it } from 'vitest' @@ -181,14 +181,14 @@ class FakeGateway { } } -const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)) +const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)) const run = ( effect: Effect.Effect, fake: FakeGateway, ) => Effect.runPromise( - effect.pipe(Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeContext.layer))), + effect.pipe(Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeServices.layer))), ) /** Runs an effect expected to fail and returns its typed expected error. */ @@ -197,10 +197,10 @@ const runFailure = async ( fake: FakeGateway, ): Promise => { const exit = await Effect.runPromiseExit( - effect.pipe(Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeContext.layer))), + effect.pipe(Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeServices.layer))), ) if (Exit.isSuccess(exit) === true) throw new Error('expected the effect to fail') - const failure = Cause.failureOption(exit.cause) + const failure = Cause.findErrorOption(exit.cause) if (Option.isNone(failure) === true) throw new Error('expected an expected failure, got a defect') return failure.value } diff --git a/packages/@overeng/notion-md/src/reconcile.ts b/packages/@overeng/notion-md/src/reconcile.ts index 0181fee8e7..4fe4382466 100644 --- a/packages/@overeng/notion-md/src/reconcile.ts +++ b/packages/@overeng/notion-md/src/reconcile.ts @@ -1,7 +1,7 @@ import { basename } from 'node:path' -import type { Path } from '@effect/platform' -import { FileSystem } from '@effect/platform' +import type { Path } from 'effect/Path' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { diff --git a/packages/@overeng/notion-md/src/state-store.test.ts b/packages/@overeng/notion-md/src/state-store.test.ts index 5a61f239e2..d6bb323b71 100644 --- a/packages/@overeng/notion-md/src/state-store.test.ts +++ b/packages/@overeng/notion-md/src/state-store.test.ts @@ -2,8 +2,8 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' -import { Path } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' +import { Path } from 'effect/Path' import { Effect, Layer, Schema } from 'effect' import { describe, expect, it } from 'vitest' @@ -30,7 +30,7 @@ import { import { TreeIndex } from './tree-index.ts' const withPath = async (fn: (path: Path.Path) => A): Promise => - Effect.runPromise(Path.Path.pipe(Effect.map(fn), Effect.provide(NodeContext.layer))) + Effect.runPromise(Path.Path.pipe(Effect.map(fn), Effect.provide(NodeServices.layer))) describe('notion-md state store path safety', () => { it('accepts content-addressed object paths under the local metadata root', async () => { @@ -56,10 +56,12 @@ describe('notion-md state store path safety', () => { }) }) -const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)) +const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)) const runStore = (effect: Effect.Effect) => - Effect.runPromise(effect.pipe(Effect.provide(Layer.mergeAll(stateStoreLayer, NodeContext.layer)))) + Effect.runPromise( + effect.pipe(Effect.provide(Layer.mergeAll(stateStoreLayer, NodeServices.layer))), + ) const withTempDir = async (fn: (dir: string) => Promise): Promise => { const dir = await mkdtemp(join(tmpdir(), 'notion-md-state-store-')) @@ -92,10 +94,10 @@ const syncStateFor = (opts: { }) const encodeJson = (schema: Schema.Schema, value: A): string => - Schema.encodeSync(Schema.parseJson(schema, { space: 2 }))(value) + Schema.encodeSync(Schema.fromJsonString(schema, { space: 2 }))(value) const decodeJson = (schema: Schema.Schema, encoded: string): A => - Schema.decodeUnknownSync(Schema.parseJson(schema), { + Schema.decodeUnknownSync(Schema.fromJsonString(schema), { errors: 'all', onExcessProperty: 'error', } as const)(encoded) @@ -967,7 +969,7 @@ describe('notion-md state store object lifecycle', () => { }) const runFs = (effect: Effect.Effect) => - Effect.runPromise(effect.pipe(Effect.provide(NodeContext.layer))) + Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))) describe('notion-md gc command discovery', () => { it('readAllSyncStates returns empty array when no sync directory exists', async () => { diff --git a/packages/@overeng/notion-md/src/state-store.ts b/packages/@overeng/notion-md/src/state-store.ts index d083a0f965..21a9c6da4b 100644 --- a/packages/@overeng/notion-md/src/state-store.ts +++ b/packages/@overeng/notion-md/src/state-store.ts @@ -1,7 +1,8 @@ import { randomUUID } from 'node:crypto' -import { FileSystem, Path } from '@effect/platform' import { Context, Effect, Layer, type ParseResult, Schema } from 'effect' +import { FileSystem } from 'effect/FileSystem' +import { Path } from 'effect/Path' import { makeNmdObjectRef, @@ -33,9 +34,9 @@ const objectRefs = (syncState: NmdSyncStateV1): readonly NmdObjectRef[] => [ export const NmdStorageObjectV2 = Schema.Struct({ version: Schema.Literal(2), page_id: Schema.String, - reason: Schema.Literal('too_large', 'volatile_url'), + reason: Schema.Literals(['too_large', 'volatile_url']), storage: NmdStorageSchema, -}).annotations({ identifier: 'NotionMd.StorageObjectV2' }) +}).annotate({ identifier: 'NotionMd.StorageObjectV2' }) export type NmdStorageObjectV2 = typeof NmdStorageObjectV2.Type @@ -45,7 +46,7 @@ export const NmdBaseSnapshotV2 = Schema.Struct({ page_id: Schema.String, body_hash: Sha256DigestSchema, body: Schema.String, -}).annotations({ identifier: 'NotionMd.BaseSnapshotV2' }) +}).annotate({ identifier: 'NotionMd.BaseSnapshotV2' }) export type NmdBaseSnapshotV2 = typeof NmdBaseSnapshotV2.Type @@ -63,20 +64,24 @@ const strictOptions = { } as const const encodeStorageObjectJson = Schema.encodeSync( - Schema.parseJson(NmdStorageObjectV2, { space: 2 }), + Schema.fromJsonString(NmdStorageObjectV2, { space: 2 }), +) +const encodeBaseSnapshotJson = Schema.encodeSync( + Schema.fromJsonString(NmdBaseSnapshotV2, { space: 2 }), +) +const encodeSyncStateJson = Schema.encodeSync( + Schema.fromJsonString(NmdSyncStateV1Schema, { space: 2 }), ) -const encodeBaseSnapshotJson = Schema.encodeSync(Schema.parseJson(NmdBaseSnapshotV2, { space: 2 })) -const encodeSyncStateJson = Schema.encodeSync(Schema.parseJson(NmdSyncStateV1Schema, { space: 2 })) const decodeStorageObjectJson = Schema.decodeUnknown( - Schema.parseJson(NmdStorageObjectV2), + Schema.fromJsonString(NmdStorageObjectV2), strictOptions, ) const decodeBaseSnapshotJson = Schema.decodeUnknown( - Schema.parseJson(NmdBaseSnapshotV2), + Schema.fromJsonString(NmdBaseSnapshotV2), strictOptions, ) const decodeSyncStateJson = Schema.decodeUnknown( - Schema.parseJson(NmdSyncStateV1Schema), + Schema.fromJsonString(NmdSyncStateV1Schema), strictOptions, ) @@ -240,10 +245,9 @@ export interface NmdStateStoreShape { } /** Service tag for the local notion-md state store. */ -export class NmdStateStore extends Context.Tag('NmdStateStore')< - NmdStateStore, - NmdStateStoreShape ->() {} +export class NmdStateStore extends Context.Service()( + 'NmdStateStore', +) {} /** Live state-store implementation backed by `@effect/platform` filesystem services. */ export const NmdStateStoreLive = Layer.effect( diff --git a/packages/@overeng/notion-md/src/stories/CliOutput.stories.tsx b/packages/@overeng/notion-md/src/stories/CliOutput.stories.tsx index 0c87b0a5e3..c44225639c 100644 --- a/packages/@overeng/notion-md/src/stories/CliOutput.stories.tsx +++ b/packages/@overeng/notion-md/src/stories/CliOutput.stories.tsx @@ -1,4 +1,4 @@ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import type { Meta, StoryObj } from '@storybook/react' import React from 'react' diff --git a/packages/@overeng/notion-md/src/stories/_fixtures.ts b/packages/@overeng/notion-md/src/stories/_fixtures.ts index 54431ab0d1..a0284aa273 100644 --- a/packages/@overeng/notion-md/src/stories/_fixtures.ts +++ b/packages/@overeng/notion-md/src/stories/_fixtures.ts @@ -1,15 +1,15 @@ import { Schema } from 'effect' -export const Severity = Schema.Literal('critical', 'warning') +export const Severity = Schema.Literals(['critical', 'warning']) export type Severity = 'critical' | 'warning' -export const CommandFixtureId = Schema.Literal( +export const CommandFixtureId = Schema.Literals([ 'clean-status', 'body-conflict', 'unknown-blocks', 'watch-sync', 'missing-token', -) +]) export type CommandFixtureId = | 'clean-status' | 'body-conflict' @@ -38,7 +38,7 @@ export type DetailSectionFixture = typeof DetailSectionFixture.Type export const MainItemFixture = Schema.Struct({ name: Schema.String, ref: Schema.String, - status: Schema.optional(Schema.Literal('error', 'modified', 'ok', 'synced')), + status: Schema.optional(Schema.Literals(['error', 'modified', 'ok', 'synced'])), relationship: Schema.optional(Schema.String), sections: Schema.Array(DetailSectionFixture), }) diff --git a/packages/@overeng/notion-md/src/sync.e2e.test.ts b/packages/@overeng/notion-md/src/sync.e2e.test.ts index e1fc8ef08b..9f43aee9d5 100644 --- a/packages/@overeng/notion-md/src/sync.e2e.test.ts +++ b/packages/@overeng/notion-md/src/sync.e2e.test.ts @@ -3,7 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Deferred, Effect, Fiber, Layer } from 'effect' import { describe, expect, it } from 'vitest' @@ -493,14 +493,14 @@ const withTempDir = async (fn: (dir: string) => Promise): Promise => { } } -const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)) +const stateStoreLayer = NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)) const runWithFake = ( effect: Effect.Effect, fake: FakeNotion, ) => Effect.runPromise( - effect.pipe(Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeContext.layer))), + effect.pipe(Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeServices.layer))), ) const runEitherWithFake = ( @@ -510,7 +510,7 @@ const runEitherWithFake = ( Effect.runPromise( effect.pipe( Effect.either, - Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeContext.layer)), + Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeServices.layer)), ), ) @@ -781,7 +781,7 @@ describe('notion-md e2e prototype', () => { yield* Deferred.await(pushed) yield* Fiber.interrupt(fiber) }), - ).pipe(Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeContext.layer))), + ).pipe(Effect.provide(Layer.mergeAll(fake.layer, stateStoreLayer, NodeServices.layer))), { inspect: { service: 'notion-md-test' } }, ), ) diff --git a/packages/@overeng/notion-md/src/sync.ts b/packages/@overeng/notion-md/src/sync.ts index d8bff1bb17..418019d46b 100644 --- a/packages/@overeng/notion-md/src/sync.ts +++ b/packages/@overeng/notion-md/src/sync.ts @@ -1,6 +1,6 @@ import { basename } from 'node:path' -import type { FileSystem } from '@effect/platform' +import type { FileSystem } from 'effect/FileSystem' import { Effect, Option } from 'effect' import { describeBodyLossyRefusal, tolerateTreeChildPages } from '@overeng/notion-core' diff --git a/packages/@overeng/notion-md/src/tree-index.ts b/packages/@overeng/notion-md/src/tree-index.ts index 7fa54b9b1c..500770590e 100644 --- a/packages/@overeng/notion-md/src/tree-index.ts +++ b/packages/@overeng/notion-md/src/tree-index.ts @@ -1,7 +1,7 @@ import { dirname, join, relative, resolve } from 'node:path' -import { FileSystem } from '@effect/platform' import { Effect, Schema } from 'effect' +import { FileSystem } from 'effect/FileSystem' import { NmdCliError, NmdFileSystemError, type NmdError } from './errors.ts' @@ -12,13 +12,13 @@ export const TreeIndex = Schema.Struct({ /** Root-file basename, so a later run reconstructs the same layout. */ root_file: Schema.String, /** posix relativePath (from root) → page_id; derived from frontmatter each run. */ - pages: Schema.Record({ key: Schema.String, value: Schema.String }), -}).annotations({ identifier: 'NotionMd.TreeIndex' }) + pages: Schema.Record(Schema.String, Schema.String), +}).annotate({ identifier: 'NotionMd.TreeIndex' }) export type TreeIndex = typeof TreeIndex.Type -const encodeTreeIndexJson = Schema.encodeSync(Schema.parseJson(TreeIndex, { space: 2 })) -const decodeTreeIndexJson = Schema.decodeUnknown(Schema.parseJson(TreeIndex), { +const encodeTreeIndexJson = Schema.encodeSync(Schema.fromJsonString(TreeIndex, { space: 2 })) +const decodeTreeIndexJson = Schema.decodeUnknown(Schema.fromJsonString(TreeIndex), { errors: 'all', onExcessProperty: 'error', } as const) diff --git a/packages/@overeng/notion-md/src/tree.ts b/packages/@overeng/notion-md/src/tree.ts index a21c31b9c1..67f180ff77 100644 --- a/packages/@overeng/notion-md/src/tree.ts +++ b/packages/@overeng/notion-md/src/tree.ts @@ -1,6 +1,6 @@ import { basename, dirname, extname, join, relative, resolve } from 'node:path' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect } from 'effect' import { describeBodyLossyRefusal, tolerateTreeChildPages } from '@overeng/notion-core' diff --git a/packages/@overeng/notion-md/src/tree.unit.test.ts b/packages/@overeng/notion-md/src/tree.unit.test.ts index adc581b018..81b1b57031 100644 --- a/packages/@overeng/notion-md/src/tree.unit.test.ts +++ b/packages/@overeng/notion-md/src/tree.unit.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer } from 'effect' import { describe, expect, it } from 'vitest' @@ -251,8 +251,8 @@ const run = ( Effect.provide( Layer.mergeAll( fake.layer, - NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)), - NodeContext.layer, + NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)), + NodeServices.layer, ), ), ), @@ -790,8 +790,8 @@ describe('notion-md tree reconcile lifecycle', () => { Effect.provide( Layer.mergeAll( fake.layer, - NmdStateStoreLive.pipe(Layer.provide(NodeContext.layer)), - NodeContext.layer, + NmdStateStoreLive.pipe(Layer.provide(NodeServices.layer)), + NodeServices.layer, ), ), ), diff --git a/packages/@overeng/notion-md/src/webhook.ts b/packages/@overeng/notion-md/src/webhook.ts index 7c07dab943..51ea16c59a 100644 --- a/packages/@overeng/notion-md/src/webhook.ts +++ b/packages/@overeng/notion-md/src/webhook.ts @@ -17,16 +17,16 @@ export const NotionWebhookPayload = NotionWebhookPayloadSchema export type NotionWebhookPayload = CanonicalNotionWebhookPayload const decodeWebhookJson = Schema.decodeUnknown( - Schema.parseJson(NotionWebhookPayload), + Schema.fromJsonString(NotionWebhookPayload), notionWebhookDecodeOptions, ) /** Expected failure while decoding or normalizing a Notion webhook payload. */ -export class NmdWebhookPayloadError extends Schema.TaggedError()( +export class NmdWebhookPayloadError extends Schema.TaggedErrorClass()( 'NmdWebhookPayloadError', { message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} diff --git a/packages/@overeng/notion-md/src/webhook.unit.test.ts b/packages/@overeng/notion-md/src/webhook.unit.test.ts index 8608d56bdd..011ee741c2 100644 --- a/packages/@overeng/notion-md/src/webhook.unit.test.ts +++ b/packages/@overeng/notion-md/src/webhook.unit.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Deferred, Effect, Fiber, Stream } from 'effect' import { describe, expect, it } from 'vitest' @@ -155,7 +155,7 @@ describe('Notion webhook trigger ingestion', () => { yield* Deferred.await(webhookSynced) yield* Fiber.interrupt(fiber) }), - ).pipe(Effect.provide(NodeContext.layer)), + ).pipe(Effect.provide(NodeServices.layer)), ) expect(events).toContainEqual( diff --git a/packages/@overeng/notion-property-write/package.json b/packages/@overeng/notion-property-write/package.json index 7891bceb22..fac8fdc124 100644 --- a/packages/@overeng/notion-property-write/package.json +++ b/packages/@overeng/notion-property-write/package.json @@ -16,15 +16,15 @@ "@overeng/notion-effect-schema": "workspace:^" }, "devDependencies": { - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/notion-property-write/src/guards.ts b/packages/@overeng/notion-property-write/src/guards.ts index 13668c89bd..9549cf421c 100644 --- a/packages/@overeng/notion-property-write/src/guards.ts +++ b/packages/@overeng/notion-property-write/src/guards.ts @@ -49,7 +49,7 @@ export const propertyWriteGuardNames = [ ] as const /** A single property-write guard name. */ -export const PropertyWriteGuardName = Schema.Literal(...propertyWriteGuardNames).annotations({ +export const PropertyWriteGuardName = Schema.Literal(...propertyWriteGuardNames).annotate({ identifier: 'Notion.PropertyWrite.GuardName', }) export type PropertyWriteGuardName = typeof PropertyWriteGuardName.Type @@ -62,13 +62,13 @@ export type PropertyWriteGuardName = typeof PropertyWriteGuardName.Type * `GuardDecision` exactly so the planner can push this decision straight into * its guard pipeline in 3c. */ -export const PropertyWriteGuardDecision = Schema.Union( +export const PropertyWriteGuardDecision = Schema.Union([ Schema.TaggedStruct('allowed', {}), Schema.TaggedStruct('blocked', { guard: PropertyWriteGuardName, message: Schema.String, }), -).annotations({ identifier: 'Notion.PropertyWrite.GuardDecision' }) +]).annotate({ identifier: 'Notion.PropertyWrite.GuardDecision' }) export type PropertyWriteGuardDecision = typeof PropertyWriteGuardDecision.Type /** Constructs the `allowed` decision (the write may proceed). */ diff --git a/packages/@overeng/notion-property-write/src/proof.ts b/packages/@overeng/notion-property-write/src/proof.ts index 5ecb93e2a1..894a0d5d3d 100644 --- a/packages/@overeng/notion-property-write/src/proof.ts +++ b/packages/@overeng/notion-property-write/src/proof.ts @@ -39,7 +39,7 @@ import { * established (e.g. shared mode sets `settlement: 'present'`, local mode sets * `settlement: 'not-required'`). */ -export const PropertyWriteMode = Schema.Literal('local', 'remote', 'shared').annotations({ +export const PropertyWriteMode = Schema.Literals(['local', 'remote', 'shared']).annotate({ identifier: 'Notion.PropertyWrite.Mode', }) export type PropertyWriteMode = typeof PropertyWriteMode.Type @@ -51,7 +51,7 @@ export const PropertyWriteIdentity = Schema.Struct({ evidenceSource: PropertyIdentityEvidenceSource, /** `false` when the resolved display name maps ambiguously to multiple properties. */ displayNameUnambiguous: Schema.Boolean, -}).annotations({ identifier: 'Notion.PropertyWrite.Identity' }) +}).annotate({ identifier: 'Notion.PropertyWrite.Identity' }) export type PropertyWriteIdentity = typeof PropertyWriteIdentity.Type /** @@ -74,36 +74,36 @@ export const PropertyWriteSchemaConsistency = Schema.Struct({ expectedConfigHash: Schema.optional(ConfigHash), propertyType: NotionPropertyType, writeClass: PropertyWriteClass, -}).annotations({ identifier: 'Notion.PropertyWrite.SchemaConsistency' }) +}).annotate({ identifier: 'Notion.PropertyWrite.SchemaConsistency' }) export type PropertyWriteSchemaConsistency = typeof PropertyWriteSchemaConsistency.Type /** Whether the property's current value surface was completely materialized. */ export const PropertyWriteBaseCompleteness = Schema.Struct({ surfaceComplete: Schema.Boolean, -}).annotations({ identifier: 'Notion.PropertyWrite.BaseCompleteness' }) +}).annotate({ identifier: 'Notion.PropertyWrite.BaseCompleteness' }) export type PropertyWriteBaseCompleteness = typeof PropertyWriteBaseCompleteness.Type /** Availability of relation targets referenced by the write (explicit literal, never optional). */ export const PropertyWriteRelationAvailability = Schema.Struct({ - status: Schema.Literal( + status: Schema.Literals([ 'not-applicable', 'all-available', 'targets-unavailable', 'related-data-source-unshared', - ), -}).annotations({ identifier: 'Notion.PropertyWrite.RelationAvailability' }) + ]), +}).annotate({ identifier: 'Notion.PropertyWrite.RelationAvailability' }) export type PropertyWriteRelationAvailability = typeof PropertyWriteRelationAvailability.Type /** Whether the local surface agrees with the freshly observed remote surface (explicit literal). */ export const PropertyWriteLocalConvergence = Schema.Struct({ - status: Schema.Literal('not-applicable', 'converged', 'disagrees'), -}).annotations({ identifier: 'Notion.PropertyWrite.LocalConvergence' }) + status: Schema.Literals(['not-applicable', 'converged', 'disagrees']), +}).annotate({ identifier: 'Notion.PropertyWrite.LocalConvergence' }) export type PropertyWriteLocalConvergence = typeof PropertyWriteLocalConvergence.Type /** Whether a required read-after-write settlement is present (explicit literal). */ export const PropertyWriteSettlement = Schema.Struct({ - status: Schema.Literal('not-required', 'present', 'missing'), -}).annotations({ identifier: 'Notion.PropertyWrite.Settlement' }) + status: Schema.Literals(['not-required', 'present', 'missing']), +}).annotate({ identifier: 'Notion.PropertyWrite.Settlement' }) export type PropertyWriteSettlement = typeof PropertyWriteSettlement.Type /** @@ -123,7 +123,7 @@ export const PropertyWriteProof = Schema.Struct({ relationAvailability: PropertyWriteRelationAvailability, localConvergence: PropertyWriteLocalConvergence, settlement: PropertyWriteSettlement, -}).annotations({ identifier: 'Notion.PropertyWrite.Proof' }) +}).annotate({ identifier: 'Notion.PropertyWrite.Proof' }) export type PropertyWriteProof = typeof PropertyWriteProof.Type /** @@ -136,7 +136,7 @@ export const DesiredPropertyWrite = Schema.Struct({ propertyId: PropertyId, dataSourceId: DataSourceId, value: CanonicalPropertyValue, -}).annotations({ identifier: 'Notion.PropertyWrite.Desired' }) +}).annotate({ identifier: 'Notion.PropertyWrite.Desired' }) export type DesiredPropertyWrite = typeof DesiredPropertyWrite.Type /** diff --git a/packages/@overeng/notion-react/package.json b/packages/@overeng/notion-react/package.json index 42b9da6fd7..4a7d218c37 100644 --- a/packages/@overeng/notion-react/package.json +++ b/packages/@overeng/notion-react/package.json @@ -34,23 +34,18 @@ "test:integration:e2e": "vitest run --config vitest.integration.config.ts e2e" }, "dependencies": { - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@overeng/notion-effect-client": "workspace:^", "@overeng/notion-effect-schema": "workspace:^", "@overeng/otel-contract": "workspace:^", "@playwright/test": "1.61.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "vitest": "4.1.9" }, "devDependencies": { - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@opentelemetry/api": "1.9.1", "@overeng/utils": "workspace:^", "@overeng/utils-dev": "workspace:^", @@ -61,7 +56,7 @@ "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@types/react-reconciler": "0.33.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "katex": "0.17.0", "react": "19.2.7", "react-dom": "19.2.7", @@ -74,7 +69,7 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.9.1", - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "katex": "^0.17.0", "react": "^19.2.7", "react-reconciler": "^0.33.0", diff --git a/packages/@overeng/notion-react/package.json.genie.ts b/packages/@overeng/notion-react/package.json.genie.ts index 638475c322..d5c1d65e66 100644 --- a/packages/@overeng/notion-react/package.json.genie.ts +++ b/packages/@overeng/notion-react/package.json.genie.ts @@ -20,28 +20,12 @@ const workspaceDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/notion-react' }), dependencies: { workspace: [notionEffectClientPkg, notionEffectSchemaPkg, otelContractPkg], - external: catalog.pick('@effect/platform'), + external: catalog.pick('effect'), }, devDependencies: { workspace: [utilsDevPkg, utilsPkg], external: { - ...catalog.pick( - ...peerDepNames, - ...optionalPeerDepNames, - '@effect/vitest', - '@storybook/react', - '@storybook/react-vite', - '@types/katex', - '@types/node', - '@types/react', - '@types/react-dom', - '@types/react-reconciler', - 'react-dom', - 'storybook', - 'typescript', - 'vite', - 'vitest', - ), + ...catalog.pick(...peerDepNames, ...optionalPeerDepNames, '@effect/vitest', '@storybook/react', '@storybook/react-vite', '@types/katex', '@types/node', '@types/react', '@types/react-dom', '@types/react-reconciler', 'react-dom', 'storybook', 'typescript', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/notion-react/src/cache/fs-cache.ts b/packages/@overeng/notion-react/src/cache/fs-cache.ts index 9c9cb8afd5..c8b0cc7236 100644 --- a/packages/@overeng/notion-react/src/cache/fs-cache.ts +++ b/packages/@overeng/notion-react/src/cache/fs-cache.ts @@ -15,10 +15,10 @@ const decode = Schema.decodeUnknown(CacheTree) * well-formed-but-stale payload can still be treated as a cold cache by the * separate schema `decode` below. */ -const parseJson = Schema.decode(Schema.parseJson()) +const parseJson = Schema.decode(Schema.fromJsonString(Schema.Unknown)) /** Encode a `CacheTree` to its on-disk JSON string (compact, schema field order). */ -const encodeJson = Schema.encode(Schema.parseJson(CacheTree)) +const encodeJson = Schema.encode(Schema.fromJsonString(CacheTree)) const readIfExists = (filePath: string): Effect.Effect => Effect.tryPromise({ diff --git a/packages/@overeng/notion-react/src/cache/types.ts b/packages/@overeng/notion-react/src/cache/types.ts index d3adf5e9a5..ae88f6d4e5 100644 --- a/packages/@overeng/notion-react/src/cache/types.ts +++ b/packages/@overeng/notion-react/src/cache/types.ts @@ -1,4 +1,4 @@ -import { Schema, type Effect } from 'effect' +import { Effect, Schema } from 'effect' import type { CacheError } from '../renderer/errors.ts' @@ -68,9 +68,10 @@ export const CacheNode: Schema.Schema = Schema.susp // `optionalWith({ default })` keeps existing v2 caches decodable: entries // serialized before this field existed default to `'block'`, which matches // the legacy "every cache node is a block" invariant. - nodeKind: Schema.optionalWith(Schema.Literal('block', 'page'), { - default: () => 'block' as const, - }), + nodeKind: Schema.Literals(['block', 'page']).pipe( + Schema.withDecodingDefaultType(Effect.sync(() => 'block' as const)), + Schema.withConstructorDefault(Effect.sync(() => 'block' as const)), + ), titleHash: Schema.optional(Schema.String), iconHash: Schema.optional(Schema.String), coverHash: Schema.optional(Schema.String), diff --git a/packages/@overeng/notion-react/src/o11y/effect-adapter.ts b/packages/@overeng/notion-react/src/o11y/effect-adapter.ts index ebd285c865..6c0995effd 100644 --- a/packages/@overeng/notion-react/src/o11y/effect-adapter.ts +++ b/packages/@overeng/notion-react/src/o11y/effect-adapter.ts @@ -1,4 +1,3 @@ -import type { HttpClient } from '@effect/platform' /** * Effect-native OTEL adapter layering on the core `onEvent` hook. * @@ -20,6 +19,7 @@ import type { HttpClient } from '@effect/platform' */ import { Cause, Context, Effect, Exit, Option, Schema } from 'effect' import type { Tracer } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import type { ReactNode } from 'react' import type { NotionConfig } from '@overeng/notion-effect-client' @@ -60,8 +60,8 @@ const shortId = (id: string): string => id.replaceAll('-', '').slice(0, 8) /** Milliseconds → nanoseconds (bigint) for Tracer.Span APIs. */ const msToNs = (ms: number): bigint => BigInt(Math.trunc(ms * 1_000_000)) -const OpKind = Schema.Literal('append', 'update', 'delete', 'retrieve') -const CacheKind = Schema.Literal('hit', 'miss', 'drift', 'page-id-drift') +const OpKind = Schema.Literals(['append', 'update', 'delete', 'retrieve']) +const CacheKind = Schema.Literals(['hit', 'miss', 'drift', 'page-id-drift']) // Bundles are REBUILT from the registered `notion-react.*` catalog schemas (SC-R13/R14 — the // contract is the single SSOT for the projection AND these runtime encoders). `service.name` diff --git a/packages/@overeng/notion-react/src/o11y/otel-adapter.ts b/packages/@overeng/notion-react/src/o11y/otel-adapter.ts index ce596d3451..cba618d39c 100644 --- a/packages/@overeng/notion-react/src/o11y/otel-adapter.ts +++ b/packages/@overeng/notion-react/src/o11y/otel-adapter.ts @@ -46,7 +46,7 @@ const shortId = (id: string): string => id.replaceAll('-', '').slice(0, 8) const STATUS_OK = 1 as SpanStatusCode const STATUS_ERROR = 2 as SpanStatusCode -const OpKind = Schema.Literal('append', 'update', 'delete', 'retrieve') +const OpKind = Schema.Literals(['append', 'update', 'delete', 'retrieve']) // Bundles are REBUILT from the registered `notion-react.*` catalog schemas (SC-R13/R14 — the // contract is the single SSOT for the projection AND these runtime encoders). `service.name` diff --git a/packages/@overeng/notion-react/src/renderer/pagination.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/pagination.unit.test.tsx index 7a1e700b57..93f0b06df3 100644 --- a/packages/@overeng/notion-react/src/renderer/pagination.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/pagination.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect, Exit } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { Fragment, type ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/renderer/render-to-notion.ts b/packages/@overeng/notion-react/src/renderer/render-to-notion.ts index eaa4ee8249..0595e94316 100644 --- a/packages/@overeng/notion-react/src/renderer/render-to-notion.ts +++ b/packages/@overeng/notion-react/src/renderer/render-to-notion.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import type { ReactNode } from 'react' import type { NotionApiError } from '@overeng/notion-effect-client' diff --git a/packages/@overeng/notion-react/src/renderer/sync-events.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/sync-events.unit.test.tsx index 250db284cc..f52556f773 100644 --- a/packages/@overeng/notion-react/src/renderer/sync-events.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/sync-events.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import type { ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/renderer/sync-metrics.churn.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/sync-metrics.churn.unit.test.tsx index 0a38386fb3..24929a2eba 100644 --- a/packages/@overeng/notion-react/src/renderer/sync-metrics.churn.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/sync-metrics.churn.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { Fragment, type ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/renderer/sync-metrics.errors.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/sync-metrics.errors.unit.test.tsx index 1ad0a23be0..743137c0eb 100644 --- a/packages/@overeng/notion-react/src/renderer/sync-metrics.errors.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/sync-metrics.errors.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect, Exit } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { Fragment, type ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/renderer/sync-metrics.matrix.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/sync-metrics.matrix.unit.test.tsx index dbe06a4fae..b7ae906fa6 100644 --- a/packages/@overeng/notion-react/src/renderer/sync-metrics.matrix.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/sync-metrics.matrix.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { Fragment, type ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/renderer/sync-metrics.scale.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/sync-metrics.scale.unit.test.tsx index 74b0190cc2..663560ea17 100644 --- a/packages/@overeng/notion-react/src/renderer/sync-metrics.scale.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/sync-metrics.scale.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { Fragment, type ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/renderer/sync-metrics.shape.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/sync-metrics.shape.unit.test.tsx index 68e2c34756..e09f4dd8f6 100644 --- a/packages/@overeng/notion-react/src/renderer/sync-metrics.shape.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/sync-metrics.shape.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { Fragment, type ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/renderer/sync-mock.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/sync-mock.unit.test.tsx index 8e20c05ec3..f43b475a0c 100644 --- a/packages/@overeng/notion-react/src/renderer/sync-mock.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/sync-mock.unit.test.tsx @@ -1,5 +1,5 @@ -import { HttpClient, HttpClientRequest } from '@effect/platform' import { Effect } from 'effect' +import { HttpClient, HttpClientRequest } from 'effect/unstable/http' import { Fragment, type ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/renderer/sync-page-ops.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/sync-page-ops.unit.test.tsx index 5399d19254..5117f639f9 100644 --- a/packages/@overeng/notion-react/src/renderer/sync-page-ops.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/sync-page-ops.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { describe, expect, it } from 'vitest' import { NotionPages, type NotionConfig } from '@overeng/notion-effect-client' diff --git a/packages/@overeng/notion-react/src/renderer/sync-property.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/sync-property.unit.test.tsx index d18074100a..5143b7ba15 100644 --- a/packages/@overeng/notion-react/src/renderer/sync-property.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/sync-property.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { Fragment, type ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/renderer/sync.ts b/packages/@overeng/notion-react/src/renderer/sync.ts index 35acbf509f..e2a5a36e18 100644 --- a/packages/@overeng/notion-react/src/renderer/sync.ts +++ b/packages/@overeng/notion-react/src/renderer/sync.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Chunk, Effect, Stream } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import type { ReactNode } from 'react' import { diff --git a/packages/@overeng/notion-react/src/renderer/upload-id-retry.unit.test.tsx b/packages/@overeng/notion-react/src/renderer/upload-id-retry.unit.test.tsx index 9bf88f730b..78fa2ba28f 100644 --- a/packages/@overeng/notion-react/src/renderer/upload-id-retry.unit.test.tsx +++ b/packages/@overeng/notion-react/src/renderer/upload-id-retry.unit.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Cause, Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { describe, expect, it } from 'vitest' import type { NotionConfig } from '@overeng/notion-effect-client' @@ -81,7 +81,7 @@ describe('onUploadIdRejected hook', () => { expect(callCount).toBe(1) // The retry attempt also failed — surfaces as upload-id-rejected reason. if (exit._tag === 'Failure') { - const err = Cause.failureOption(exit.cause) + const err = Cause.findErrorOption(exit.cause) expect(err._tag).toBe('Some') if (err._tag === 'Some') { expect((err.value as NotionSyncError).reason).toBe('notion-upload-id-rejected') @@ -101,7 +101,7 @@ describe('onUploadIdRejected hook', () => { ) expect(exit._tag).toBe('Failure') if (exit._tag === 'Failure') { - const err = Cause.failureOption(exit.cause) + const err = Cause.findErrorOption(exit.cause) expect(err._tag).toBe('Some') if (err._tag === 'Some') { expect((err.value as NotionSyncError).reason).toBe('notion-upload-id-rejected') @@ -151,7 +151,7 @@ describe('onUploadIdRejected hook', () => { ) expect(exit._tag).toBe('Failure') if (exit._tag === 'Failure') { - const err = Cause.failureOption(exit.cause) + const err = Cause.findErrorOption(exit.cause) expect(err._tag).toBe('Some') if (err._tag === 'Some') { expect((err.value as NotionSyncError).reason).toBe('consumer-reupload-failed') diff --git a/packages/@overeng/notion-react/src/test/integration/e2e/helpers.ts b/packages/@overeng/notion-react/src/test/integration/e2e/helpers.ts index c5620be21a..ed433e7eb3 100644 --- a/packages/@overeng/notion-react/src/test/integration/e2e/helpers.ts +++ b/packages/@overeng/notion-react/src/test/integration/e2e/helpers.ts @@ -1,5 +1,5 @@ -import { FetchHttpClient, type HttpClient } from '@effect/platform' import { Duration, Effect, Layer, Redacted, Schedule } from 'effect' +import { FetchHttpClient, type HttpClient } from 'effect/unstable/http' import type { NotionApiError } from '@overeng/notion-effect-client' import { NotionBlocks, NotionConfig, NotionPages } from '@overeng/notion-effect-client' diff --git a/packages/@overeng/notion-react/src/test/integration/e2e/mutations.e2e.test.tsx b/packages/@overeng/notion-react/src/test/integration/e2e/mutations.e2e.test.tsx index ddd3b3b81b..e35e81551f 100644 --- a/packages/@overeng/notion-react/src/test/integration/e2e/mutations.e2e.test.tsx +++ b/packages/@overeng/notion-react/src/test/integration/e2e/mutations.e2e.test.tsx @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import type { ReactNode } from 'react' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/notion-react/src/test/mock-client.ts b/packages/@overeng/notion-react/src/test/mock-client.ts index 503d559da4..63bf9035b2 100644 --- a/packages/@overeng/notion-react/src/test/mock-client.ts +++ b/packages/@overeng/notion-react/src/test/mock-client.ts @@ -1,5 +1,5 @@ -import { HttpClient, type HttpClientRequest, HttpClientResponse } from '@effect/platform' import { Effect, Layer, Redacted, Schema } from 'effect' +import { HttpClient, type HttpClientRequest, HttpClientResponse } from 'effect/unstable/http' import { NotionConfig } from '@overeng/notion-effect-client' @@ -12,9 +12,9 @@ const NotionErrorEnvelope = Schema.Struct({ }) /** Encode an arbitrary handler result to a JSON response string. */ -const encodeResponseJson = Schema.encodeSync(Schema.parseJson()) +const encodeResponseJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)) /** Encode a Notion error envelope to its JSON response string. */ -const encodeErrorEnvelopeJson = Schema.encodeSync(Schema.parseJson(NotionErrorEnvelope)) +const encodeErrorEnvelopeJson = Schema.encodeSync(Schema.fromJsonString(NotionErrorEnvelope)) /** * In-memory Notion API stub for driver-level tests. diff --git a/packages/@overeng/notion-react/src/test/mock-client.unit.test.ts b/packages/@overeng/notion-react/src/test/mock-client.unit.test.ts index e352ed4c95..05c9583a45 100644 --- a/packages/@overeng/notion-react/src/test/mock-client.unit.test.ts +++ b/packages/@overeng/notion-react/src/test/mock-client.unit.test.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { describe, expect, it } from 'vitest' import { NotionBlocks, type NotionConfig, NotionPages } from '@overeng/notion-effect-client' diff --git a/packages/@overeng/notion-react/src/test/regression/sync-perf.regression.unit.test.tsx b/packages/@overeng/notion-react/src/test/regression/sync-perf.regression.unit.test.tsx index 0827d9e816..cbe4864899 100644 --- a/packages/@overeng/notion-react/src/test/regression/sync-perf.regression.unit.test.tsx +++ b/packages/@overeng/notion-react/src/test/regression/sync-perf.regression.unit.test.tsx @@ -1,8 +1,8 @@ import { writeFileSync, mkdirSync } from 'node:fs' import { dirname, join } from 'node:path' -import type { HttpClient } from '@effect/platform' import { Effect } from 'effect' +import type { HttpClient } from 'effect/unstable/http' import { Fragment, type ReactNode } from 'react' import { afterAll, describe, expect, it } from 'vitest' diff --git a/packages/@overeng/npm-release/nix/build.nix b/packages/@overeng/npm-release/nix/build.nix index abd58caa78..d8d4c31577 100644 --- a/packages/@overeng/npm-release/nix/build.nix +++ b/packages/@overeng/npm-release/nix/build.nix @@ -20,7 +20,7 @@ let workspaceRoot = src; # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { - "." = mkSharedHash "sha256-evw+K1akyqdC5nttRINpUF3xlZ1ASp1qsdrNs4lKVQ0="; + "." = mkSharedHash "sha256-X0BB6p311WKbs54MBLsw8yxJJUiFjb3ya5L9DzpCqM0="; }; smokeTestArgs = [ "--help" ]; inherit gitRev commitTs dirty; diff --git a/packages/@overeng/npm-release/package.json b/packages/@overeng/npm-release/package.json index 12c269dda4..c26c537fa5 100644 --- a/packages/@overeng/npm-release/package.json +++ b/packages/@overeng/npm-release/package.json @@ -15,19 +15,15 @@ } }, "devDependencies": { - "@effect/cli": "0.75.2", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", + "@effect/platform-node": "4.0.0-beta.102", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "@effect/cli": "^0.75.2", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "effect": "^3.21.4" + "@effect/platform-node": "^4.0.0-beta.102", + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/npm-release/package.json.genie.ts b/packages/@overeng/npm-release/package.json.genie.ts index d80fe88831..8e38155f3d 100644 --- a/packages/@overeng/npm-release/package.json.genie.ts +++ b/packages/@overeng/npm-release/package.json.genie.ts @@ -13,7 +13,7 @@ import { * that only classify registry state pull in no dependencies. Effect appears here for * the `./cli` entry, which is bundled into a standalone binary at build time. */ -const peerDepNames = ['@effect/cli', '@effect/platform', '@effect/platform-node', 'effect'] as const +const peerDepNames = ['@effect/platform-node', 'effect'] as const const workspaceDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/npm-release' }), diff --git a/packages/@overeng/npm-release/src/cli.ts b/packages/@overeng/npm-release/src/cli.ts index 5d42dbb6a0..bf4d6460dd 100644 --- a/packages/@overeng/npm-release/src/cli.ts +++ b/packages/@overeng/npm-release/src/cli.ts @@ -9,9 +9,9 @@ * and `megarepo`. */ -import { Command as Cli, Options } from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Cause, Duration, Effect, Option, Schedule } from 'effect' +import { Command as Cli, Flag as Options } from 'effect/unstable/cli' import { readPlan, verifyPlan, type VerifyFailure } from './verify.ts' @@ -21,7 +21,7 @@ const planOption = Options.file('plan').pipe( ), ) -const registryOption = Options.text('registry').pipe( +const registryOption = Options.string('registry').pipe( Options.withDefault('https://registry.npmjs.org'), Options.withDescription('Registry to query'), ) @@ -91,7 +91,7 @@ const cli = Cli.run(Cli.make('npm-release').pipe(Cli.withSubcommands([verifyComm const reportUnexpected = (cause: Cause.Cause) => Cause.isInterruptedOnly(cause) === true ? Effect.void - : Option.match(Cause.failureOption(cause), { + : Option.match(Cause.findErrorOption(cause), { onNone: () => Effect.logError(Cause.pretty(cause)), onSome: (error) => isRendered(error) === true ? Effect.void : Effect.logError(String(error)), @@ -107,7 +107,7 @@ const isRendered = (error: unknown) => if (import.meta.main) { cli(process.argv).pipe( Effect.tapErrorCause(reportUnexpected), - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), NodeRuntime.runMain({ disableErrorReporting: true }), ) } diff --git a/packages/@overeng/npm-release/src/verify.ts b/packages/@overeng/npm-release/src/verify.ts index 868160b32b..25360a0dc1 100644 --- a/packages/@overeng/npm-release/src/verify.ts +++ b/packages/@overeng/npm-release/src/verify.ts @@ -8,25 +8,26 @@ import { createHash } from 'node:crypto' -import { Command, FileSystem } from '@effect/platform' import { Effect, Schema } from 'effect' import type { Schedule } from 'effect' +import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command } from 'effect/unstable/process' import { registryVerification, type RemoteRegistryState } from './mod.ts' /** One package to verify. `tarball` is absent when this run did not pack it. */ export const PlanPackage = Schema.Struct({ - name: Schema.NonEmptyTrimmedString, - tarball: Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'NpmRelease.PlanPackage' }) + name: Schema.Trimmed.check(Schema.isNonEmpty()), + tarball: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'NpmRelease.PlanPackage' }) /** The release a caller wants the registry to agree with. */ export const VerifyPlan = Schema.Struct({ schemaVersion: Schema.Literal(1), - version: Schema.NonEmptyTrimmedString, - npmTag: Schema.NonEmptyTrimmedString, + version: Schema.Trimmed.check(Schema.isNonEmpty()), + npmTag: Schema.Trimmed.check(Schema.isNonEmpty()), packages: Schema.NonEmptyArray(PlanPackage), -}).annotations({ identifier: 'NpmRelease.VerifyPlan' }) +}).annotate({ identifier: 'NpmRelease.VerifyPlan' }) export type VerifyPlan = typeof VerifyPlan.Type @@ -35,18 +36,18 @@ export const VerifyFailure = Schema.Struct({ package: Schema.String, reason: Schema.String, terminal: Schema.Boolean, -}).annotations({ identifier: 'NpmRelease.VerifyFailure' }) +}).annotate({ identifier: 'NpmRelease.VerifyFailure' }) export type VerifyFailure = typeof VerifyFailure.Type /** Raised when the plan file cannot be read or does not decode. */ -export class PlanError extends Schema.TaggedError()('PlanError', { +export class PlanError extends Schema.TaggedErrorClass()('PlanError', { path: Schema.String, message: Schema.String, }) {} /** Raised when the registry does not serve the release the plan describes. */ -export class VerificationFailed extends Schema.TaggedError()( +export class VerificationFailed extends Schema.TaggedErrorClass()( 'VerificationFailed', { failures: Schema.Array(VerifyFailure), @@ -55,7 +56,7 @@ export class VerificationFailed extends Schema.TaggedError() ) {} /** Registry state has not converged yet; retried until the budget is spent. */ -class NotConverged extends Schema.TaggedError()('NotConverged', { +class NotConverged extends Schema.TaggedErrorClass()('NotConverged', { reason: Schema.String, }) {} @@ -64,7 +65,7 @@ const RegistryManifest = Schema.Struct({ dist: Schema.optional(Schema.Struct({ integrity: Schema.optional(Schema.String) })), }) -const RegistryDistTags = Schema.Record({ key: Schema.String, value: Schema.String }) +const RegistryDistTags = Schema.Record(Schema.String, Schema.String) /** * `npm view --json`, with every failure mode reported as absent data. @@ -83,7 +84,7 @@ const npmViewJson = Effect.fn('npmViewJson')(function* ( Effect.orElseSucceed(() => ''), ) - return yield* Schema.decodeUnknown(Schema.parseJson(schema))(raw.trim()).pipe( + return yield* Schema.decodeUnknown(Schema.fromJsonString(schema))(raw.trim()).pipe( Effect.orElseSucceed(() => undefined), ) }) @@ -244,7 +245,7 @@ export const readPlan = Effect.fn('readPlan')(function* (path: string) { ), ) - return yield* Schema.decodeUnknown(Schema.parseJson(VerifyPlan))(content).pipe( + return yield* Schema.decodeUnknown(Schema.fromJsonString(VerifyPlan))(content).pipe( Effect.mapError((cause) => new PlanError({ path, message: `Invalid plan: ${cause}` })), ) }) diff --git a/packages/@overeng/otel-contract/package.json b/packages/@overeng/otel-contract/package.json index 8641cc0d09..3510104285 100644 --- a/packages/@overeng/otel-contract/package.json +++ b/packages/@overeng/otel-contract/package.json @@ -20,12 +20,12 @@ "devDependencies": { "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/otel-contract/src/mod.ts b/packages/@overeng/otel-contract/src/mod.ts index aacc526441..7542f00028 100644 --- a/packages/@overeng/otel-contract/src/mod.ts +++ b/packages/@overeng/otel-contract/src/mod.ts @@ -3,14 +3,15 @@ import { DateTime, Duration, Effect, - Either, Exit, Metric, - MetricBoundaries, Option, - ParseResult, Redacted, + Result, Schema, + SchemaGetter, + SchemaIssue, + SchemaParser, Stream, } from 'effect' import * as AST from 'effect/SchemaAST' @@ -18,57 +19,59 @@ import * as AST from 'effect/SchemaAST' export * from './otel-scrape/registry.gen.ts' export * from './otel-scrape/profile-link.ts' +type AnyNoContext = Schema.Codec + type OtelPrimitive = string | number | boolean /** Branded OTel attribute key: letter-led, `[A-Za-z0-9_.:-]`, ≤255 chars — the canonical key shape shared by resource and span attributes. */ -export const OtelAttributeKey = Schema.NonEmptyTrimmedString.pipe( - Schema.maxLength(255), - Schema.pattern(/^[A-Za-z][A-Za-z0-9_.:-]*$/), +export const OtelAttributeKey = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( + Schema.check(Schema.isMaxLength(255)), + Schema.check(Schema.isPattern(/^[A-Za-z][A-Za-z0-9_.:-]*$/)), Schema.brand('OtelAttributeKey'), - Schema.annotations({ identifier: 'Otel.AttributeKey' }), + Schema.annotate({ identifier: 'Otel.AttributeKey' }), ) export type OtelAttributeKey = typeof OtelAttributeKey.Type /** Branded span name: any printable ASCII (`[ -~]`, so spaces/punctuation allowed unlike keys), ≤255 chars. */ -export const OtelSpanName = Schema.NonEmptyTrimmedString.pipe( - Schema.maxLength(255), - Schema.pattern(/^[ -~]+$/), +export const OtelSpanName = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( + Schema.check(Schema.isMaxLength(255)), + Schema.check(Schema.isPattern(/^[ -~]+$/)), Schema.brand('OtelSpanName'), - Schema.annotations({ identifier: 'Otel.SpanName' }), + Schema.annotate({ identifier: 'Otel.SpanName' }), ) export type OtelSpanName = typeof OtelSpanName.Type /** Branded metric name: Prometheus-style, may lead with `_` or `:` (not just a letter), ≤255 chars. */ -export const OtelMetricName = Schema.NonEmptyTrimmedString.pipe( - Schema.maxLength(255), - Schema.pattern(/^[A-Za-z_:][A-Za-z0-9_.:-]*$/), +export const OtelMetricName = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( + Schema.check(Schema.isMaxLength(255)), + Schema.check(Schema.isPattern(/^[A-Za-z_:][A-Za-z0-9_.:-]*$/)), Schema.brand('OtelMetricName'), - Schema.annotations({ identifier: 'Otel.MetricName' }), + Schema.annotate({ identifier: 'Otel.MetricName' }), ) export type OtelMetricName = typeof OtelMetricName.Type /** Branded `service.name` resource value: letter-led, `[A-Za-z0-9_.:-]`, ≤255 chars — the telemetry service identity. */ -export const OtelServiceName = Schema.NonEmptyTrimmedString.pipe( - Schema.maxLength(255), - Schema.pattern(/^[A-Za-z][A-Za-z0-9_.:-]*$/), +export const OtelServiceName = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( + Schema.check(Schema.isMaxLength(255)), + Schema.check(Schema.isPattern(/^[A-Za-z][A-Za-z0-9_.:-]*$/)), Schema.brand('OtelServiceName'), - Schema.annotations({ identifier: 'Otel.ServiceName' }), + Schema.annotate({ identifier: 'Otel.ServiceName' }), ) export type OtelServiceName = typeof OtelServiceName.Type /** Resource attribute key `service.namespace` — a logical grouping for related services. */ -export const OtelServiceNamespace = Schema.NonEmptyTrimmedString.pipe( - Schema.maxLength(255), +export const OtelServiceNamespace = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( + Schema.check(Schema.isMaxLength(255)), Schema.brand('OtelServiceNamespace'), - Schema.annotations({ identifier: 'Otel.ServiceNamespace' }), + Schema.annotate({ identifier: 'Otel.ServiceNamespace' }), ) export type OtelServiceNamespace = typeof OtelServiceNamespace.Type /** Resource attribute key `service.version` — the build/release version of the service. */ -export const OtelServiceVersion = Schema.NonEmptyTrimmedString.pipe( - Schema.maxLength(255), +export const OtelServiceVersion = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( + Schema.check(Schema.isMaxLength(255)), Schema.brand('OtelServiceVersion'), - Schema.annotations({ identifier: 'Otel.ServiceVersion' }), + Schema.annotate({ identifier: 'Otel.ServiceVersion' }), ) export type OtelServiceVersion = typeof OtelServiceVersion.Type @@ -82,12 +85,12 @@ export const ServiceIdentity = Schema.Struct({ name: OtelServiceName, namespace: OtelServiceNamespace, version: OtelServiceVersion, -}).annotations({ identifier: 'Otel.ServiceIdentity' }) +}).annotate({ identifier: 'Otel.ServiceIdentity' }) export type ServiceIdentity = typeof ServiceIdentity.Type /** * Pre-validation parts of the conventional `-` service name. The - * parts are plain {@link Schema.NonEmptyTrimmedString} rather than dedicated + * parts are plain non-empty trimmed strings rather than dedicated * brands: that is the LIGHTEST typing that still rejects empty/whitespace parts, * and it is load-bearing for correctness. {@link OtelServiceName}'s pattern * (`^[A-Za-z][A-Za-z0-9_.:-]*$`) admits a TRAILING hyphen, so an empty `role` @@ -97,9 +100,9 @@ export type ServiceIdentity = typeof ServiceIdentity.Type * naming law (shared with the rest of the contract) still applies. */ export const ServiceNameParts = Schema.Struct({ - project: Schema.NonEmptyTrimmedString, - role: Schema.NonEmptyTrimmedString, -}).annotations({ identifier: 'Otel.ServiceNameParts' }) + project: Schema.Trimmed.check(Schema.isNonEmpty()), + role: Schema.Trimmed.check(Schema.isNonEmpty()), +}).annotate({ identifier: 'Otel.ServiceNameParts' }) export type ServiceNameParts = typeof ServiceNameParts.Type /** @@ -108,26 +111,37 @@ export type ServiceNameParts = typeof ServiceNameParts.Type * the joined string decodes through the {@link OtelServiceName} brand (the same * naming law as every other contract name). A malformed part is a decode error at * the composition root, never a backend surprise. Decode it like any other brand: - * `Schema.decode(ServiceNameFromParts)({ project, role })`. + * `Schema.decodeEffect(ServiceNameFromParts)({ project, role })`. */ -export const ServiceNameFromParts = Schema.transformOrFail(ServiceNameParts, OtelServiceName, { - strict: true, - decode: (parts, _options, ast) => - ParseResult.decodeUnknown(OtelServiceName)(`${parts.project}-${parts.role}`).pipe( +const serviceNameFromPartsTransformation = { + decode: SchemaGetter.transformOrFail((parts) => + SchemaParser.decodeUnknownEffect(OtelServiceName)(`${parts.project}-${parts.role}`).pipe( Effect.mapError( (issue) => - new ParseResult.Type(ast, parts, ParseResult.TreeFormatter.formatIssueSync(issue)), + new SchemaIssue.InvalidValue(Option.some(parts), { + message: SchemaIssue.makeFormatterDefault()(issue), + }), ), ), - encode: (name, _options, ast) => - ParseResult.fail( - new ParseResult.Forbidden( - ast, - name, - 'A composed service name cannot be split back into parts', - ), + ), + encode: SchemaGetter.transformOrFail((name) => + Effect.fail( + new SchemaIssue.Forbidden(Option.some(name), { + message: 'A composed service name cannot be split back into parts', + }), ), -}).annotations({ identifier: 'Otel.ServiceNameFromParts' }) + ), +} + +export const ServiceNameFromParts = ServiceNameParts.pipe( + Schema.decodeTo( + OtelServiceName, + { + decode: serviceNameFromPartsTransformation.decode, + encode: serviceNameFromPartsTransformation.encode, + }, + ), +).annotate({ identifier: 'Otel.ServiceNameFromParts' }) /** * The SHAPE a private fleet configuration supplies to produce a @@ -153,19 +167,19 @@ export interface FleetServiceBinding { * Assembles a validated {@link ServiceIdentity} from a {@link FleetServiceBinding}: * the `-` name is built + validated via {@link ServiceNameFromParts} * and `namespace`/`version` decode through their brands. Removes the hand-rolled - * `Schema.decode(ServiceIdentity)({ name: `${project}-${role}`, … })` at every + * `Schema.decodeEffect(ServiceIdentity)({ name: `${project}-${role}`, … })` at every * composition root. A malformed part/namespace/version is a decode error here, at * the edge. */ export const serviceIdentityFromBinding = ( binding: FleetServiceBinding, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { - const name = yield* Schema.decode(ServiceNameFromParts)({ + const name = yield* Schema.decodeEffect(ServiceNameFromParts)({ project: binding.project, role: binding.role, }) - return yield* Schema.decode(ServiceIdentity)({ + return yield* Schema.decodeEffect(ServiceIdentity)({ name, namespace: binding.namespace, version: binding.version, @@ -200,7 +214,7 @@ export interface OtelAttrMetadata { export const OtelAttrAnnotationId: unique symbol = Symbol.for('@overeng/utils/otel/Attr') /** Raised when `OtelAttrs.define` cannot derive a safe field plan from a schema. */ -export class OtelAttrPlanError extends Schema.TaggedError()( +export class OtelAttrPlanError extends Schema.TaggedErrorClass()( 'OtelAttrPlanError', { path: Schema.Array(Schema.String), @@ -209,12 +223,12 @@ export class OtelAttrPlanError extends Schema.TaggedError()( ) {} /** Raised when a value cannot be encoded as an OTEL attribute. */ -export class OtelAttrEncodeError extends Schema.TaggedError()( +export class OtelAttrEncodeError extends Schema.TaggedErrorClass()( 'OtelAttrEncodeError', { key: Schema.String, message: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) {} @@ -223,24 +237,24 @@ type FieldEncoder = ( ) => Effect.Effect const decodeNameSync = (options: { - readonly schema: Schema.Schema + readonly schema: Schema.Codec readonly value: string readonly path: ReadonlyArray readonly kind: string }): A => { const decoded = decodeNameEither(options) - if (Either.isRight(decoded) === true) return decoded.right - throw decoded.left + if (Result.isSuccess(decoded) === true) return decoded.success + throw decoded.failure } const decodeNameEither = (options: { - readonly schema: Schema.Schema + readonly schema: Schema.Codec readonly value: string readonly path: ReadonlyArray readonly kind: string -}): Either.Either => - Schema.decodeUnknownEither(options.schema)(options.value).pipe( - Either.mapLeft(() => +}): Result.Result => + Schema.decodeUnknownResult(options.schema)(options.value).pipe( + Result.mapError(() => unsupported({ path: options.path, message: `Invalid OTEL ${options.kind}: ${options.value}`, @@ -294,7 +308,7 @@ interface FieldPlan { } /** Compiled schema-backed OTEL attribute contract. */ -export interface OtelAttrs { +export interface OtelAttrs { readonly schema: S readonly keys: ReadonlySet readonly fields: ReadonlyArray @@ -307,7 +321,7 @@ export interface OtelAttrs { } /** Named span contract coupled to a compiled attribute schema. */ -export interface OtelSpanDefinition { +export interface OtelSpanDefinition { readonly name: string readonly attributes: OtelAttrs readonly root?: boolean @@ -335,7 +349,7 @@ export interface OtelOperationMetadata { } /** Named operation contract: the normal schema-first API for product code. */ -export interface OtelOperationDefinition { +export interface OtelOperationDefinition { readonly name: string readonly attributes: OtelAttrs readonly root?: boolean @@ -383,7 +397,7 @@ export interface OtelMetricLabelsMetadata { } /** Schema-backed metric labels. Metric labels intentionally use stricter cardinality policy than spans. */ -export interface OtelMetricLabels { +export interface OtelMetricLabels { readonly schema: S readonly attributes: OtelAttrs readonly metadata: OtelMetricLabelsMetadata @@ -410,7 +424,7 @@ export interface OtelMetricMetadata { } /** Runtime-light metric contract. It owns names, labels, cardinality, and metadata, not emission. */ -export interface OtelMetricDefinition { +export interface OtelMetricDefinition { readonly instrument: OtelMetricInstrumentKind readonly name: string readonly description?: string @@ -424,15 +438,15 @@ export interface OtelMetricDefinition { readonly unsafeEncodeLabels: (value: Schema.Schema.Type) => OtelAttributeMap readonly tagPairs: ( value: Schema.Schema.Type, - ) => Effect.Effect, OtelAttrEncodeError> + ) => Effect.Effect, OtelAttrEncodeError> readonly trustedTagPairs: ( value: Schema.Schema.Type, - ) => Effect.Effect> + ) => Effect.Effect> } /** Metric definition narrowed to a histogram, adding optional explicit bucket `boundaries`. */ export interface OtelHistogramDefinition< - S extends Schema.Schema.AnyNoContext, + S extends AnyNoContext, > extends OtelMetricDefinition { readonly instrument: 'histogram' readonly boundaries?: ReadonlyArray @@ -440,20 +454,20 @@ export interface OtelHistogramDefinition< /** Metric definition narrowed to a gauge (instantaneous last-set value, no boundaries). */ export interface OtelGaugeDefinition< - S extends Schema.Schema.AnyNoContext, + S extends AnyNoContext, > extends OtelMetricDefinition { readonly instrument: 'gauge' } /** Alias for the underlying Effect counter runtime that a counter contract drives. */ -export type OtelEffectCounterMetric = Metric.Metric.Counter +export type OtelEffectCounterMetric = Metric.Counter /** Alias for the underlying Effect histogram runtime that a histogram contract drives. */ -export type OtelEffectHistogramMetric = Metric.Metric.Histogram +export type OtelEffectHistogramMetric = Metric.Histogram /** Alias for the underlying Effect gauge runtime that a gauge contract drives. */ -export type OtelEffectGaugeMetric = Metric.Metric.Gauge +export type OtelEffectGaugeMetric = Metric.Gauge /** Effect Metric runtime bridge for a schema-first counter contract. */ -export interface OtelEffectCounter { +export interface OtelEffectCounter { readonly definition: OtelMetricDefinition readonly metric: OtelEffectCounterMetric readonly increment: (labels: Schema.Schema.Type) => Effect.Effect @@ -469,7 +483,7 @@ export interface OtelEffectCounter { } /** Effect Metric runtime bridge for a schema-first histogram contract. */ -export interface OtelEffectHistogram { +export interface OtelEffectHistogram { readonly definition: OtelHistogramDefinition readonly metric: OtelEffectHistogramMetric readonly record: (options: { @@ -483,7 +497,7 @@ export interface OtelEffectHistogram { } /** Effect Metric runtime bridge for a schema-first gauge contract. */ -export interface OtelEffectGauge { +export interface OtelEffectGauge { readonly definition: OtelGaugeDefinition readonly metric: OtelEffectGaugeMetric readonly set: (options: { @@ -496,17 +510,20 @@ export interface OtelEffectGauge { }) => Effect.Effect } -const getAttrMetadata = (annotated: AST.Annotated): OtelAttrMetadata | undefined => - Option.getOrUndefined(AST.getAnnotation(annotated, OtelAttrAnnotationId)) +const getAttrMetadata = (annotated: AST.AST): OtelAttrMetadata | undefined => + (AST.resolve(annotated) as Record | undefined)?.[ + OtelAttrAnnotationId + ] as OtelAttrMetadata | undefined + +const getPropertyAttrMetadata = (ast: AST.AST): OtelAttrMetadata | undefined => + (ast.context?.annotations as Record | undefined)?.[ + OtelAttrAnnotationId + ] as OtelAttrMetadata | undefined const getAttrMetadataDeep = (ast: AST.AST): OtelAttrMetadata | undefined => { const metadata = getAttrMetadata(ast) if (metadata !== undefined) return metadata switch (ast._tag) { - case 'Refinement': - return getAttrMetadataDeep(ast.from) - case 'Transformation': - return getAttrMetadataDeep(ast.to) ?? getAttrMetadataDeep(ast.from) case 'Union': return ast.types .filter((member) => isUndefinedAst(member) === false) @@ -519,33 +536,13 @@ const getAttrMetadataDeep = (ast: AST.AST): OtelAttrMetadata | undefined => { const withAttrMetadata = (metadata: OtelAttrMetadata) => - (schema: S): Schema.Annotable.Self => - Schema.make, Schema.Schema.Encoded, Schema.Schema.Context>( - addAnnotation({ ast: schema.ast, metadata }), - ) as Schema.Annotable.Self - -const addAnnotation = ({ - ast, - metadata, -}: { - readonly ast: AST.AST - readonly metadata: OtelAttrMetadata -}): AST.AST => { - const descriptors: PropertyDescriptorMap = Object.getOwnPropertyDescriptors(ast) - descriptors.annotations = { - configurable: true, - enumerable: true, - value: { - ...ast.annotations, + (schema: S): S['Rebuild'] => + schema.annotate({ [OtelAttrAnnotationId]: { - ...getAttrMetadata(ast), + ...getAttrMetadata(schema.ast), ...metadata, }, - }, - writable: true, - } - return Object.create(Object.getPrototypeOf(ast), descriptors) as AST.AST -} + }) /** Schema annotation helpers for deriving OTEL attribute keys and encoding policies. */ export const OtelAttr = { @@ -562,14 +559,14 @@ export const OtelAttr = { }: { key: string metadata?: Omit - }): Schema.Schema => Schema.String.pipe(OtelAttr.key({ ...metadata, key })), + }): Schema.Codec => Schema.String.pipe(OtelAttr.key({ ...metadata, key })), boolean: ({ key, metadata = {}, }: { key: string metadata?: Omit - }): Schema.Schema => + }): Schema.Codec => Schema.Boolean.pipe(OtelAttr.key({ cardinality: 'low', ...metadata, key })), number: ({ key, @@ -577,18 +574,18 @@ export const OtelAttr = { }: { key: string metadata?: Omit - }): Schema.Schema => Schema.Number.pipe(OtelAttr.key({ ...metadata, key })), + }): Schema.Codec => Schema.Number.pipe(OtelAttr.key({ ...metadata, key })), literal: ]>( key: string, ...values: Literals - ): Schema.Literal => - Schema.Literal(...values).pipe( + ): Schema.Literals => + Schema.Literals(values).pipe( OtelAttr.key({ key, cardinality: values.length <= 2 ? 'low' : 'bounded' }), - ) as Schema.Literal, - optional: (schema: S) => Schema.optional(schema), - redacted: (key: string): Schema.Schema, string, never> => - Schema.Redacted(Schema.String).pipe(OtelAttr.key({ key, encode: 'redacted' })), - json: ({ + ) as Schema.Literals, + optional: (schema: S) => Schema.optional(schema), + redacted: (key: string): Schema.Codec, string, never> => + Schema.RedactedFromValue(Schema.String).pipe(OtelAttr.key({ key, encode: 'redacted' })), + json: ({ key, schema, metadata = {}, @@ -597,7 +594,7 @@ export const OtelAttr = { schema: S metadata?: Omit }): S => schema.pipe(OtelAttr.key({ ...metadata, key, encode: 'json' })) as S, - drop: (schema: S): S => + drop: (schema: S): S => schema.pipe(OtelAttr.encode('drop')) as S, } as const @@ -645,7 +642,7 @@ const primitiveFromUnknown = ({ readonly value: unknown }) => { if (typeof value === 'number' && isFiniteOtelNumber(value) === false) { - return Either.left( + return Result.fail( new OtelAttrEncodeError({ key, message: `OTEL number attribute ${key} must be finite`, @@ -653,25 +650,25 @@ const primitiveFromUnknown = ({ ) } return isPrimitive(value) === true - ? Either.right(value) - : Either.left(primitiveEncodeError({ key, value })) + ? Result.succeed(value) + : Result.fail(primitiveEncodeError({ key, value })) } -const effectFromEither = (either: Either.Either): Effect.Effect => - Either.isRight(either) === true ? Effect.succeed(either.right) : Effect.fail(either.left) +const effectFromEither = (either: Result.Result): Effect.Effect => + Result.isSuccess(either) === true ? Effect.succeed(either.success) : Effect.fail(either.failure) const runSyncOrThrow = (effect: Effect.Effect): A => Exit.match(Effect.runSyncExit(effect), { onSuccess: (value) => value, onFailure: (cause) => { - const failure = Option.getOrUndefined(Cause.failureOption(cause)) + const failure = Option.getOrUndefined(Cause.findErrorOption(cause)) if (failure !== undefined) throw failure throw Cause.squash(cause) }, }) /** Codec whose encode side mirrors `JSON.stringify` for arbitrary values. */ -const jsonStringFromUnknown = Schema.parseJson(Schema.Unknown) +const jsonStringFromUnknown = Schema.fromJsonString(Schema.Unknown) const encodeUnknown = ({ key, @@ -679,42 +676,65 @@ const encodeUnknown = ({ value, }: { readonly key: string - readonly schema: Schema.Schema + readonly schema: Schema.Codec readonly value: unknown }) => - effectFromEither(Schema.encodeUnknownEither(schema)(value)).pipe( + effectFromEither(Schema.encodeUnknownResult(schema)(value)).pipe( Effect.mapError((cause) => encodeFailure({ key, cause })), ) const astIdentifier = (ast: AST.AST): string | undefined => - Option.getOrUndefined(AST.getIdentifierAnnotation(ast)) + AST.resolveIdentifier(ast) ?? + Option.getOrUndefined( + (AST as typeof AST & { + readonly getIdentifierAnnotation?: (ast: AST.AST) => Option.Option + }).getIdentifierAnnotation?.(ast) ?? Option.none(), + ) -const typeConstructorTag = (ast: AST.AST): string | undefined => - Option.getOrUndefined(AST.getTypeConstructorAnnotation(ast))?._tag +const typeConstructorTag = (ast: AST.AST): string | undefined => { + const representation = AST.resolve(ast)?.representation as { readonly id?: string } | undefined + const representationId = representation?.id + switch (representationId) { + case 'effect/schema/Redacted': + return 'effect/Redacted' + case 'effect/schema/Option': + return 'effect/Option' + case 'effect/schema/Duration': + return 'effect/Duration' + case 'effect/schema/DateTimeUtc': + return 'effect/DateTime.Utc' + default: { + const legacyAnnotation = ( + AST as typeof AST & { + readonly getTypeConstructorAnnotation?: ( + ast: AST.AST, + ) => Option.Option<{ readonly _tag: string }> + } + ).getTypeConstructorAnnotation?.(ast) + return legacyAnnotation === undefined ? undefined : Option.getOrUndefined(legacyAnnotation)?._tag + } + } +} -const typeConstructorTagDeep = (ast: AST.AST): string | undefined => - typeConstructorTag(ast) ?? - (ast._tag === 'Transformation' ? typeConstructorTagDeep(ast.to) : undefined) +const typeConstructorTagDeep = (ast: AST.AST): string | undefined => typeConstructorTag(ast) const typeConstructorParametersDeep = (ast: AST.AST): ReadonlyArray => { if (ast._tag === 'Declaration') return ast.typeParameters - if (ast._tag === 'Transformation') return typeConstructorParametersDeep(ast.to) return [] } -const unwrapRefinement = (ast: AST.AST): AST.AST => - ast._tag === 'Refinement' ? unwrapRefinement(ast.from) : ast +const unwrapRefinement = (ast: AST.AST): AST.AST => ast const isUndefinedAst = (ast: AST.AST): boolean => - ast._tag === 'UndefinedKeyword' || + ast._tag === 'Undefined' || (ast._tag === 'Union' && ast.types.some((member) => isUndefinedAst(member))) const isPrimitiveAst = (ast: AST.AST): boolean => { const unwrapped = unwrapRefinement(ast) switch (unwrapped._tag) { - case 'StringKeyword': - case 'NumberKeyword': - case 'BooleanKeyword': + case 'String': + case 'Number': + case 'Boolean': return true case 'Literal': return isPrimitive(unwrapped.literal) @@ -734,7 +754,7 @@ const inferCardinality = ( ): NonNullable | undefined => { const unwrapped = unwrapRefinement(ast) switch (unwrapped._tag) { - case 'BooleanKeyword': + case 'Boolean': return 'low' case 'Literal': return typeof unwrapped.literal === 'boolean' ? 'low' : 'bounded' @@ -751,10 +771,9 @@ const inferCardinality = ( } } -const rootTypeLiteral = (schema: Schema.Schema.AnyNoContext) => { +const rootTypeLiteral = (schema: AnyNoContext) => { const ast = schema.ast - if (ast._tag === 'TypeLiteral') return ast - if (ast._tag === 'Transformation' && ast.to._tag === 'TypeLiteral') return ast.to + if (ast._tag === 'Objects') return ast return undefined } @@ -765,7 +784,7 @@ const compileAutoEncoder = ({ }: { readonly attrKey: string readonly path: ReadonlyArray - readonly schema: Schema.Schema + readonly schema: Schema.Codec }): Effect.Effect => { const ast = schema.ast const tag = typeConstructorTagDeep(ast) @@ -790,7 +809,8 @@ const compileAutoEncoder = ({ ) } if (tag === 'effect/Duration') { - if (astIdentifier(ast) !== 'DurationFromMillis') { + const identifier = astIdentifier(ast) + if (identifier !== undefined && identifier !== 'DurationFromMillis') { return Effect.fail( unsupported({ path, @@ -799,18 +819,18 @@ const compileAutoEncoder = ({ ) } return Effect.succeed((value) => - Effect.succeed(Duration.toMillis(value as Duration.DurationInput)), + Effect.succeed(Duration.toMillis(value as Duration.Input)), ) } if (tag === 'effect/DateTime.Utc') { return Effect.succeed((value) => Effect.succeed(DateTime.formatIso(value as DateTime.Utc))) } - if (ast._tag === 'TypeLiteral') { + if (ast._tag === 'Objects') { return Effect.fail( unsupported({ path, message: 'Nested Struct attributes require an explicit encoder' }), ) } - if (ast._tag === 'TupleType') { + if (ast._tag === 'Arrays') { return Effect.fail( unsupported({ path, @@ -818,7 +838,7 @@ const compileAutoEncoder = ({ }), ) } - if (isPrimitiveAst(ast) === false && ast._tag !== 'Transformation') { + if (isPrimitiveAst(ast) === false) { return Effect.fail( unsupported({ path, message: `Unsupported OTEL attribute schema: ${String(ast)}` }), ) @@ -843,7 +863,7 @@ const compilePolicyEncoder = ({ }: { readonly attrKey: string readonly policy: Exclude - readonly schema: Schema.Schema + readonly schema: Schema.Codec }): FieldEncoder => { switch (policy) { case 'drop': @@ -858,10 +878,10 @@ const compilePolicyEncoder = ({ return (value) => encodeUnknown({ key: attrKey, schema, value }).pipe( Effect.flatMap((encoded) => - // `Schema.parseJson(Schema.Unknown)` encodes via `JSON.stringify`, but + // `Schema.fromJsonString(Schema.Unknown)` encodes via `JSON.stringify`, but // fails with a `ParseError` exactly when the result would be `undefined` // (functions, bare `undefined`, ...) instead of returning `undefined`. - Schema.encode(jsonStringFromUnknown)(encoded).pipe( + Schema.encodeEffect(jsonStringFromUnknown)(encoded).pipe( Effect.mapError((cause) => encodeFailure({ key: attrKey, cause })), ), ), @@ -895,8 +915,8 @@ const compilePolicyEncoder = ({ const compileField = ( field: AST.PropertySignature, ): Effect.Effect => { - const metadata = getAttrMetadataDeep(field.type) ?? getAttrMetadata(field) - const fieldSchema = Schema.make(field.type) + const metadata = getPropertyAttrMetadata(field.type) ?? getAttrMetadataDeep(field.type) + const fieldSchema = Schema.make>(field.type) return Effect.gen(function* () { const attrKey = yield* decodeAttributeKey(metadata?.key ?? String(field.name)) const tag = typeConstructorTagDeep(field.type) @@ -923,7 +943,7 @@ const compileField = ( sourceKey: field.name, attrKey, ...(metadata?.role === undefined ? {} : { role: metadata.role }), - optional: field.isOptional || isUndefinedAst(field.type), + optional: AST.isOptional(field.type) || isUndefinedAst(field.type), encodePolicy, ...(cardinality === undefined ? {} : { cardinality }), ...(schemaIdentifier === undefined ? {} : { schemaIdentifier }), @@ -945,7 +965,7 @@ const fieldMetadata = (field: FieldPlan): OtelAttrFieldMetadata => ({ }) const compilePlan = ( - schema: Schema.Schema.AnyNoContext, + schema: AnyNoContext, ): Effect.Effect, OtelAttrPlanError> => Effect.gen(function* () { const root = rootTypeLiteral(schema) @@ -977,7 +997,7 @@ const compilePlan = ( /** Constructors for schema-backed OTEL attribute contracts. */ export const OtelAttrs = { - define( + define( schema: S, ): Effect.Effect, OtelAttrPlanError> { return Effect.gen(function* () { @@ -1007,21 +1027,21 @@ export const OtelAttrs = { } }) }, - defineSync(schema: S): OtelAttrs { + defineSync(schema: S): OtelAttrs { return runSyncOrThrow(OtelAttrs.define(schema)) }, } -function withSpanContract(options: { +function withSpanContract(options: { readonly span: OtelSpanDefinition readonly attributes: Schema.Schema.Type readonly effect: Effect.Effect }): Effect.Effect -function withSpanContract(options: { +function withSpanContract(options: { readonly span: OtelSpanDefinition readonly attributes: Schema.Schema.Type }): (effect: Effect.Effect) => Effect.Effect -function withSpanContract(options: { +function withSpanContract(options: { readonly span: OtelSpanDefinition readonly attributes: Schema.Schema.Type readonly effect?: Effect.Effect @@ -1040,16 +1060,16 @@ function withSpanContract(options return options.effect === undefined ? wrap : wrap(options.effect) } -function unsafeWithSpanContract(options: { +function unsafeWithSpanContract(options: { readonly span: OtelSpanDefinition readonly attributes: Schema.Schema.Type readonly effect: Effect.Effect }): Effect.Effect -function unsafeWithSpanContract(options: { +function unsafeWithSpanContract(options: { readonly span: OtelSpanDefinition readonly attributes: Schema.Schema.Type }): (effect: Effect.Effect) => Effect.Effect -function unsafeWithSpanContract(options: { +function unsafeWithSpanContract(options: { readonly span: OtelSpanDefinition readonly attributes: Schema.Schema.Type readonly effect?: Effect.Effect @@ -1064,16 +1084,16 @@ function unsafeWithSpanContract(o return options.effect === undefined ? wrap : wrap(options.effect) } -function withStreamSpanContract(options: { +function withStreamSpanContract(options: { readonly span: OtelSpanDefinition readonly attributes: Schema.Schema.Type readonly stream: Stream.Stream }): Stream.Stream -function withStreamSpanContract(options: { +function withStreamSpanContract(options: { readonly span: OtelSpanDefinition readonly attributes: Schema.Schema.Type }): (stream: Stream.Stream) => Stream.Stream -function withStreamSpanContract(options: { +function withStreamSpanContract(options: { readonly span: OtelSpanDefinition readonly attributes: Schema.Schema.Type readonly stream?: Stream.Stream @@ -1094,7 +1114,7 @@ function withStreamSpanContract(o return options.stream === undefined ? wrap : wrap(options.stream) } -const spanMetadata = ( +const spanMetadata = ( options: Omit, 'metadata'>, ): OtelSpanMetadata => ({ kind: 'span', @@ -1105,20 +1125,20 @@ const spanMetadata = ( hasSpanLabel: options.attributes.hasSpanLabel, }) -const normalizeSpanLabel = (label: string): Either.Either => { +const normalizeSpanLabel = (label: string): Result.Result => { const normalized = label.trim() if (normalized.length === 0) { - return Either.left( + return Result.fail( new OtelAttrEncodeError({ key: 'span.label', message: 'OtelOperation label must be a non-empty string', }), ) } - return Either.right(normalized) + return Result.succeed(normalized) } -const operationMetadata = (options: { +const operationMetadata = (options: { readonly name: string readonly root?: boolean readonly attributes: OtelAttrs @@ -1131,7 +1151,7 @@ const operationMetadata = (options: { derivesSpanLabel: true, }) -const metricLabelsMetadata = ( +const metricLabelsMetadata = ( attributes: OtelAttrs, ): OtelMetricLabelsMetadata => ({ kind: 'metric.labels', @@ -1151,7 +1171,7 @@ const invalidMetricLabel = ({ message, }) -const assertMetricLabels = ( +const assertMetricLabels = ( attributes: OtelAttrs, ): OtelMetricLabels => { for (const field of attributes.fields) { @@ -1185,7 +1205,7 @@ const assertMetricLabels = ( } } -const metricMetadata = (options: { +const metricMetadata = (options: { readonly instrument: OtelMetricInstrumentKind readonly name: string readonly description?: string @@ -1226,7 +1246,7 @@ const validateHistogramBoundaries = ( return boundaries } -const encodeOperationAttributes = (options: { +const encodeOperationAttributes = (options: { readonly attributes: OtelAttrs readonly label: (value: Schema.Schema.Type) => string readonly value: Schema.Schema.Type @@ -1237,7 +1257,7 @@ const encodeOperationAttributes = (options return { ...attributes, 'span.label': label } }) -const isEffectOperationCall = ( +const isEffectOperationCall = ( call: | { readonly attributes: Schema.Schema.Type @@ -1249,7 +1269,7 @@ const isEffectOperationCall = ( readonly effect: Effect.Effect } => typeof call === 'object' && call !== null && 'attributes' in call && 'effect' in call -const isStreamOperationCall = ( +const isStreamOperationCall = ( call: | { readonly attributes: Schema.Schema.Type @@ -1261,19 +1281,19 @@ const isStreamOperationCall = ( readonly stream: Stream.Stream } => typeof call === 'object' && call !== null && 'attributes' in call && 'stream' in call -function defineOperation(options: { +function defineOperation(options: { readonly name: string readonly schema: S readonly label: (value: Schema.Schema.Type) => string readonly root?: boolean }): OtelOperationDefinition -function defineOperation(options: { +function defineOperation(options: { readonly name: string readonly attributes: OtelAttrs readonly label: (value: Schema.Schema.Type) => string readonly root?: boolean }): OtelOperationDefinition -function defineOperation( +function defineOperation( options: | { readonly name: string @@ -1385,40 +1405,42 @@ function defineOperation( } } -const defineMetricLabels = (schema: S): OtelMetricLabels => +const defineMetricLabels = (schema: S): OtelMetricLabels => assertMetricLabels(OtelAttrs.defineSync(schema)) -const metricLabelsFromInput = ( +const metricLabelsFromInput = ( labels: S | OtelMetricLabels, ): OtelMetricLabels => ('metadata' in labels ? labels : defineMetricLabels(labels)) const metricTagPairs = - ( + ( encodeLabels: ( value: Schema.Schema.Type, ) => Effect.Effect, ) => ( value: Schema.Schema.Type, - ): Effect.Effect, OtelAttrEncodeError> => + ): Effect.Effect, OtelAttrEncodeError> => encodeLabels(value).pipe( Effect.map((encoded) => - Object.entries(encoded).map(([key, labelValue]) => [key, String(labelValue)] as const), + Object.entries(encoded).map( + ([key, labelValue]): [string, string] => [key, String(labelValue)], + ), ), ) const trustedMetricTagPairs = - ( + ( encodeLabels: ( value: Schema.Schema.Type, ) => Effect.Effect, ) => - (value: Schema.Schema.Type): Effect.Effect> => + (value: Schema.Schema.Type): Effect.Effect> => metricTagPairs(encodeLabels)(value).pipe( Effect.catchTag('OtelAttrEncodeError', (error) => Effect.die(error)), - ) as Effect.Effect> + ) as Effect.Effect> -const defineCounter = (options: { +const defineCounter = (options: { readonly name: string readonly description?: string readonly unit?: string @@ -1448,7 +1470,7 @@ const defineCounter = (options: { } } -const defineHistogram = (options: { +const defineHistogram = (options: { readonly name: string readonly description?: string readonly unit?: string @@ -1482,7 +1504,7 @@ const defineHistogram = (options: { } } -const defineGauge = (options: { +const defineGauge = (options: { readonly name: string readonly description?: string readonly unit?: string @@ -1512,24 +1534,20 @@ const defineGauge = (options: { } } -const taggedMetric = ({ +const taggedMetric = ({ metric, tags, }: { - metric: Metric.Metric - tags: ReadonlyArray -}): Metric.Metric => - tags.reduce>( - (tagged, [key, value]) => Metric.tagged(tagged, key, value), - metric, - ) + metric: Metric.Metric + tags: ReadonlyArray<[string, string]> +}): Metric.Metric => Metric.withAttributes(metric, tags) const trustedMetricEmission = ( effect: Effect.Effect, ): Effect.Effect => effect.pipe(Effect.catchTag('OtelAttrEncodeError', (error) => Effect.die(error))) -const effectCounter = ( +const effectCounter = ( definition: OtelMetricDefinition, ): OtelEffectCounter => { if (definition.instrument !== 'counter') { @@ -1545,7 +1563,7 @@ const effectCounter = ( const incrementBy = ({ labels, amount }: { labels: Schema.Schema.Type; amount: number }) => Effect.gen(function* () { const tags = yield* definition.tagPairs(labels) - yield* Metric.incrementBy(taggedMetric({ metric, tags }), amount) + yield* Metric.update(taggedMetric({ metric, tags }), amount) }) return { @@ -1559,14 +1577,13 @@ const effectCounter = ( } } -const effectHistogram = ( +const effectHistogram = ( definition: OtelHistogramDefinition, ): OtelEffectHistogram => { - const metric = Metric.histogram( - definition.name, - MetricBoundaries.fromIterable(definition.boundaries ?? []), - definition.description, - ) + const metric = Metric.histogram(definition.name, { + boundaries: Metric.boundariesFromIterable(definition.boundaries ?? []), + ...(definition.description === undefined ? {} : { description: definition.description }), + }) const record = ({ labels, value }: { labels: Schema.Schema.Type; value: number }) => Effect.gen(function* () { const tags = yield* definition.tagPairs(labels) @@ -1581,7 +1598,7 @@ const effectHistogram = ( } } -const effectGauge = ( +const effectGauge = ( definition: OtelGaugeDefinition, ): OtelEffectGauge => { if (definition.instrument !== 'gauge') { @@ -1597,7 +1614,7 @@ const effectGauge = ( const set = ({ labels, value }: { labels: Schema.Schema.Type; value: number }) => Effect.gen(function* () { const tags = yield* definition.tagPairs(labels) - yield* Metric.set(taggedMetric({ metric, tags }), value) + yield* Metric.update(taggedMetric({ metric, tags }), value) }) return { @@ -1610,7 +1627,7 @@ const effectGauge = ( /** Helpers for applying schema-backed span contracts to Effects. */ export const OtelSpan = { - defineSync(options: { + defineSync(options: { readonly name: string readonly schema: S readonly root?: boolean @@ -1621,7 +1638,7 @@ export const OtelSpan = { ...(options.root === undefined ? {} : { root: options.root }), }) }, - define(options: { + define(options: { readonly name: string readonly attributes: OtelAttrs readonly root?: boolean @@ -1643,7 +1660,7 @@ export const OtelSpan = { with: withSpanContract, withStream: withStreamSpanContract, unsafeWith: unsafeWithSpanContract, - annotate(options: { + annotate(options: { readonly attributes: OtelAttrs readonly value: Schema.Schema.Type }): Effect.Effect { @@ -1659,7 +1676,7 @@ export const OtelSpan = { { discard: true }, ) }, - unsafeAnnotate(options: { + unsafeAnnotate(options: { readonly attributes: OtelAttrs readonly value: Schema.Schema.Type }): Effect.Effect { diff --git a/packages/@overeng/otel-contract/src/mod.unit.test.ts b/packages/@overeng/otel-contract/src/mod.unit.test.ts index 3ab75f7462..c4105175e0 100644 --- a/packages/@overeng/otel-contract/src/mod.unit.test.ts +++ b/packages/@overeng/otel-contract/src/mod.unit.test.ts @@ -24,26 +24,26 @@ import { describe('OTEL schema names', () => { it('exports branded refined schemas for contract names and keys', async () => { await expect( - Effect.runPromise(Schema.decodeUnknown(OtelAttributeKey)('service.name')), + Effect.runPromise(Schema.decodeUnknownEffect(OtelAttributeKey)('service.name')), ).resolves.toBe('service.name') await expect( - Effect.runPromise(Schema.decodeUnknown(OtelAttributeKey)('notion-react.page_id')), + Effect.runPromise(Schema.decodeUnknownEffect(OtelAttributeKey)('notion-react.page_id')), ).resolves.toBe('notion-react.page_id') await expect( - Effect.runPromise(Schema.decodeUnknown(OtelSpanName)('notion-md.pull-page')), + Effect.runPromise(Schema.decodeUnknownEffect(OtelSpanName)('notion-md.pull-page')), ).resolves.toBe('notion-md.pull-page') await expect( - Effect.runPromise(Schema.decodeUnknown(OtelMetricName)('restate_invocations_total')), + Effect.runPromise(Schema.decodeUnknownEffect(OtelMetricName)('restate_invocations_total')), ).resolves.toBe('restate_invocations_total') await expect( - Effect.runPromise(Schema.decodeUnknown(OtelServiceName)('notion-md-cli')), + Effect.runPromise(Schema.decodeUnknownEffect(OtelServiceName)('notion-md-cli')), ).resolves.toBe('notion-md-cli') }) it('rejects invalid contract names and attribute keys at definition time', async () => { await expect( Effect.runPromise( - Effect.either( + Effect.result( OtelAttrs.define( Schema.Struct({ value: OtelAttr.string({ key: 'bad key' }), @@ -52,8 +52,8 @@ describe('OTEL schema names', () => { ), ), ).resolves.toMatchObject({ - _tag: 'Left', - left: expect.any(OtelAttrPlanError), + _tag: 'Failure', + failure: expect.any(OtelAttrPlanError), }) const SpanAttrs = OtelAttrs.defineSync( @@ -84,7 +84,7 @@ describe('OTEL schema names', () => { describe('ServiceIdentity', () => { it('decodes a valid identity into branded name/namespace/version', async () => { const identity = await Effect.runPromise( - Schema.decodeUnknown(ServiceIdentity)({ + Schema.decodeUnknownEffect(ServiceIdentity)({ name: 'megarepo', namespace: 'overeng', version: '1.2.3', @@ -96,15 +96,15 @@ describe('ServiceIdentity', () => { it('rejects an invalid (non-pattern) service name', async () => { await expect( Effect.runPromise( - Effect.either( - Schema.decodeUnknown(ServiceIdentity)({ + Effect.result( + Schema.decodeUnknownEffect(ServiceIdentity)({ name: 'bad name', namespace: 'overeng', version: '1.0.0', }), ), ), - ).resolves.toMatchObject({ _tag: 'Left' }) + ).resolves.toMatchObject({ _tag: 'Failure' }) }) it('rejects empty namespace/version', async () => { @@ -113,8 +113,8 @@ describe('ServiceIdentity', () => { { name: 'svc', namespace: 'overeng', version: '' }, ]) { await expect( - Effect.runPromise(Effect.either(Schema.decodeUnknown(ServiceIdentity)(bad))), - ).resolves.toMatchObject({ _tag: 'Left' }) + Effect.runPromise(Effect.result(Schema.decodeUnknownEffect(ServiceIdentity)(bad))), + ).resolves.toMatchObject({ _tag: 'Failure' }) } }) }) @@ -122,11 +122,11 @@ describe('ServiceIdentity', () => { describe('ServiceNameFromParts', () => { it('builds `-` and validates it through the OtelServiceName brand', async () => { const name = await Effect.runPromise( - Schema.decode(ServiceNameFromParts)({ project: 'my-project', role: 'worker' }), + Schema.decodeEffect(ServiceNameFromParts)({ project: 'my-project', role: 'worker' }), ) expect(name).toBe('my-project-worker') // The result is a real OtelServiceName (decodes through the brand unchanged). - await expect(Effect.runPromise(Schema.decodeUnknown(OtelServiceName)(name))).resolves.toBe( + await expect(Effect.runPromise(Schema.decodeUnknownEffect(OtelServiceName)(name))).resolves.toBe( 'my-project-worker', ) }) @@ -142,8 +142,8 @@ describe('ServiceNameFromParts', () => { { project: ' ', role: 'worker' }, ]) { await expect( - Effect.runPromise(Effect.either(Schema.decode(ServiceNameFromParts)(bad))), - ).resolves.toMatchObject({ _tag: 'Left' }) + Effect.runPromise(Effect.result(Schema.decodeEffect(ServiceNameFromParts)(bad))), + ).resolves.toMatchObject({ _tag: 'Failure' }) } }) @@ -151,9 +151,9 @@ describe('ServiceNameFromParts', () => { // A leading digit project breaks the brand's `^[A-Za-z]` law once joined. await expect( Effect.runPromise( - Effect.either(Schema.decode(ServiceNameFromParts)({ project: '1bad', role: 'worker' })), + Effect.result(Schema.decodeEffect(ServiceNameFromParts)({ project: '1bad', role: 'worker' })), ), - ).resolves.toMatchObject({ _tag: 'Left' }) + ).resolves.toMatchObject({ _tag: 'Failure' }) }) }) @@ -169,7 +169,7 @@ describe('serviceIdentityFromBinding', () => { expect(identity).toEqual({ name: 'my-project-worker', namespace: 'acme', version: '1.2.3' }) // Re-decoding through the struct confirms the result is a valid ServiceIdentity. await expect( - Effect.runPromise(Schema.decodeUnknown(ServiceIdentity)(identity)), + Effect.runPromise(Schema.decodeUnknownEffect(ServiceIdentity)(identity)), ).resolves.toEqual(identity) }) @@ -180,8 +180,8 @@ describe('serviceIdentityFromBinding', () => { { project: 'my-project', role: 'worker', namespace: 'acme', version: '' }, ] satisfies ReadonlyArray) { await expect( - Effect.runPromise(Effect.either(serviceIdentityFromBinding(bad))), - ).resolves.toMatchObject({ _tag: 'Left' }) + Effect.runPromise(Effect.result(serviceIdentityFromBinding(bad))), + ).resolves.toMatchObject({ _tag: 'Failure' }) } }) }) @@ -189,12 +189,12 @@ describe('serviceIdentityFromBinding', () => { describe('OtelAttrs', () => { it('derives primitive, literal, uuid, option, date, duration, and explicit array attributes', async () => { const Attrs = Schema.Struct({ - label: Schema.NonEmptyTrimmedString.pipe(OtelAttr.spanLabel()), - requestId: Schema.UUID.pipe(OtelAttr.key({ key: 'request.id' })), - outcome: Schema.Literal('approved', 'denied', 'timeout').pipe( + label: Schema.Trimmed.check(Schema.isNonEmpty()).pipe(OtelAttr.spanLabel()), + requestId: Schema.String.check(Schema.isGUID()).pipe(OtelAttr.key({ key: 'request.id' })), + outcome: Schema.Literals(['approved', 'denied', 'timeout']).pipe( OtelAttr.key({ key: 'op.outcome' }), ), - count: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: 'op.count' })), + count: Schema.Natural.pipe(OtelAttr.key({ key: 'op.count' })), cacheHit: Schema.Boolean.pipe(OtelAttr.key({ key: 'op.cache_hit' })), maybeShard: Schema.OptionFromNullOr(Schema.String).pipe(OtelAttr.key({ key: 'op.shard' })), at: Schema.DateTimeUtc.pipe(OtelAttr.key({ key: 'op.at' })), @@ -202,7 +202,7 @@ describe('OtelAttrs', () => { tags: Schema.Array(Schema.String).pipe(OtelAttr.key({ key: 'op.tags', encode: 'json' })), }) const attrs = await Effect.runPromise(OtelAttrs.define(Attrs)) - const at = DateTime.unsafeMake('2026-06-11T10:00:00.000Z') + const at = DateTime.makeUnsafe('2026-06-11T10:00:00.000Z') await expect( Effect.runPromise( @@ -250,7 +250,7 @@ describe('OtelAttrs', () => { it('rejects unsafe schemas unless policy is explicit', async () => { await expect( Effect.runPromise( - Effect.either( + Effect.result( OtelAttrs.define( Schema.Struct({ nested: Schema.Struct({ id: Schema.String }).pipe(OtelAttr.key({ key: 'nested' })), @@ -259,28 +259,28 @@ describe('OtelAttrs', () => { ), ), ).resolves.toMatchObject({ - _tag: 'Left', - left: expect.any(OtelAttrPlanError), + _tag: 'Failure', + failure: expect.any(OtelAttrPlanError), }) await expect( Effect.runPromise( - Effect.either( + Effect.result( OtelAttrs.define( Schema.Struct({ - secret: Schema.Redacted(Schema.String).pipe(OtelAttr.key({ key: 'secret' })), + secret: Schema.RedactedFromValue(Schema.String).pipe(OtelAttr.key({ key: 'secret' })), }), ), ), ), ).resolves.toMatchObject({ - _tag: 'Left', - left: expect.any(OtelAttrPlanError), + _tag: 'Failure', + failure: expect.any(OtelAttrPlanError), }) await expect( Effect.runPromise( - Effect.either( + Effect.result( OtelAttrs.define( Schema.Struct({ tags: Schema.Array(Schema.String).pipe(OtelAttr.key({ key: 'tags' })), @@ -289,14 +289,14 @@ describe('OtelAttrs', () => { ), ), ).resolves.toMatchObject({ - _tag: 'Left', - left: expect.any(OtelAttrPlanError), + _tag: 'Failure', + failure: expect.any(OtelAttrPlanError), }) }) it('allows explicit redacted and json policies', async () => { const Attrs = Schema.Struct({ - secret: Schema.Redacted(Schema.String).pipe( + secret: Schema.RedactedFromValue(Schema.String).pipe( OtelAttr.key({ key: 'secret', encode: 'redacted' }), ), nested: Schema.Struct({ id: Schema.String }).pipe( @@ -321,10 +321,10 @@ describe('OtelAttrs', () => { it('only allows redacted-safe policies for redacted values', async () => { await expect( Effect.runPromise( - Effect.either( + Effect.result( OtelAttrs.define( Schema.Struct({ - secret: Schema.Redacted(Schema.String).pipe( + secret: Schema.RedactedFromValue(Schema.String).pipe( OtelAttr.key({ key: 'secret', encode: 'json' }), ), }), @@ -332,14 +332,14 @@ describe('OtelAttrs', () => { ), ), ).resolves.toMatchObject({ - _tag: 'Left', - left: expect.any(OtelAttrPlanError), + _tag: 'Failure', + failure: expect.any(OtelAttrPlanError), }) const attrs = await Effect.runPromise( OtelAttrs.define( Schema.Struct({ - secret: Schema.Redacted(Schema.String).pipe( + secret: Schema.RedactedFromValue(Schema.String).pipe( OtelAttr.key({ key: 'secret', encode: 'drop' }), ), }), @@ -358,10 +358,10 @@ describe('OtelAttrs', () => { const attrs = await Effect.runPromise(OtelAttrs.define(Attrs)) await expect( - Effect.runPromise(Effect.either(attrs.encode({ count: Number.NaN }))), + Effect.runPromise(Effect.result(attrs.encode({ count: Number.NaN }))), ).resolves.toMatchObject({ - _tag: 'Left', - left: expect.any(OtelAttrEncodeError), + _tag: 'Failure', + failure: expect.any(OtelAttrEncodeError), }) }) @@ -391,10 +391,10 @@ describe('OtelAttrs', () => { asJson: Schema.Struct({ id: Schema.String }).pipe( OtelAttr.key({ key: 'json', encode: 'json' }), ), - asString: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: 'string', encode: 'string' })), - asNumber: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: 'number', encode: 'number' })), + asString: Schema.Natural.pipe(OtelAttr.key({ key: 'string', encode: 'string' })), + asNumber: Schema.Natural.pipe(OtelAttr.key({ key: 'number', encode: 'number' })), asBoolean: Schema.Boolean.pipe(OtelAttr.key({ key: 'boolean', encode: 'boolean' })), - secret: Schema.Redacted(Schema.String).pipe( + secret: Schema.RedactedFromValue(Schema.String).pipe( OtelAttr.key({ key: 'secret', encode: 'redacted' }), ), }) @@ -439,14 +439,14 @@ describe('OtelAttrs', () => { ] const results = await Promise.all( invalidInputs.map((invalid) => - Effect.runPromise(Effect.either(attrs.encode(invalid as never))), + Effect.runPromise(Effect.result(attrs.encode(invalid as never))), ), ) for (const result of results) { expect(result).toMatchObject({ - _tag: 'Left', - left: expect.any(OtelAttrEncodeError), + _tag: 'Failure', + failure: expect.any(OtelAttrEncodeError), }) } }) @@ -456,7 +456,7 @@ describe('OtelAttrs', () => { OtelAttrs.define( Schema.Struct({ label: Schema.String.pipe(OtelAttr.spanLabel()), - count: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: 'retry.count' })), + count: Schema.Natural.pipe(OtelAttr.key({ key: 'retry.count' })), }), ), ) @@ -498,7 +498,7 @@ describe('OtelAttrs', () => { const attrs = await Effect.runPromise( OtelAttrs.define( Schema.Struct({ - label: Schema.NonEmptyTrimmedString.pipe(OtelAttr.spanLabel()), + label: Schema.Trimmed.check(Schema.isNonEmpty()).pipe(OtelAttr.spanLabel()), outcome: OtelAttr.literal('op.outcome', 'success', 'retryable', 'terminal'), cacheHit: OtelAttr.boolean({ key: 'op.cache_hit' }), requestId: OtelAttr.string({ key: 'request.id', metadata: { cardinality: 'high' } }), @@ -513,12 +513,11 @@ describe('OtelAttrs', () => { expect(attrs.fields).toMatchInlineSnapshot(` [ { - "astTag": "Refinement", + "astTag": "String", "attrKey": "span.label", "encodePolicy": "auto", "optional": false, "role": "span.label", - "schemaIdentifier": "NonEmptyTrimmedString", "sourceKey": "label", }, { @@ -530,7 +529,7 @@ describe('OtelAttrs', () => { "sourceKey": "outcome", }, { - "astTag": "BooleanKeyword", + "astTag": "Boolean", "attrKey": "op.cache_hit", "cardinality": "low", "encodePolicy": "auto", @@ -538,7 +537,7 @@ describe('OtelAttrs', () => { "sourceKey": "cacheHit", }, { - "astTag": "StringKeyword", + "astTag": "String", "attrKey": "request.id", "cardinality": "high", "encodePolicy": "auto", @@ -546,7 +545,7 @@ describe('OtelAttrs', () => { "sourceKey": "requestId", }, { - "astTag": "TypeLiteral", + "astTag": "Objects", "attrKey": "op.payload", "encodePolicy": "json", "optional": false, @@ -619,7 +618,7 @@ describe('OtelSpan', () => { await expect( Effect.runPromise( - Effect.either( + Effect.result( OtelSpan.with({ span, attributes: {}, @@ -628,8 +627,8 @@ describe('OtelSpan', () => { ), ), ).resolves.toMatchObject({ - _tag: 'Left', - left: expect.any(OtelAttrEncodeError), + _tag: 'Failure', + failure: expect.any(OtelAttrEncodeError), }) }) @@ -735,10 +734,10 @@ describe('OtelOperation', () => { }) await expect( - Effect.runPromise(Effect.either(Operation.encode({ value: 'ok' }))), + Effect.runPromise(Effect.result(Operation.encode({ value: 'ok' }))), ).resolves.toMatchObject({ - _tag: 'Left', - left: expect.any(OtelAttrEncodeError), + _tag: 'Failure', + failure: expect.any(OtelAttrEncodeError), }) }) @@ -820,7 +819,7 @@ describe('OtelMetric', () => { ], "labels": [ { - "astTag": "StringKeyword", + "astTag": "String", "attrKey": "restate.service", "cardinality": "bounded", "encodePolicy": "auto", @@ -828,7 +827,7 @@ describe('OtelMetric', () => { "sourceKey": "service", }, { - "astTag": "StringKeyword", + "astTag": "String", "attrKey": "restate.handler", "cardinality": "bounded", "encodePolicy": "auto", @@ -844,7 +843,7 @@ describe('OtelMetric', () => { "sourceKey": "outcome", }, { - "astTag": "BooleanKeyword", + "astTag": "Boolean", "attrKey": "restate.cache_hit", "cardinality": "low", "encodePolicy": "auto", @@ -944,7 +943,7 @@ describe('OtelMetric', () => { }), }) expect(Gauge.name).toBe('store_gc_rss_bytes') - await expect(Effect.runPromise(Schema.decodeUnknown(OtelMetricName)(Gauge.name))).resolves.toBe( + await expect(Effect.runPromise(Schema.decodeUnknownEffect(OtelMetricName)(Gauge.name))).resolves.toBe( 'store_gc_rss_bytes', ) expect(() => OtelMetric.gauge({ name: ' ', labels: Schema.Struct({}) })).toThrow() @@ -981,20 +980,20 @@ describe('OtelMetric', () => { }) const bridge = OtelMetric.effect.gauge(Gauge) - const snapshotValue = () => { - const entry = Metric.unsafeSnapshot(undefined).find((candidate) => { - if (candidate.metricKey.name !== 'otel_contract_test_bridge_rss_bytes') return false - const tags = Object.fromEntries(candidate.metricKey.tags.map((tag) => [tag.key, tag.value])) - return tags.operation === 'gc' - }) - return (entry?.metricState as { readonly value?: number } | undefined)?.value + const snapshotValue = async () => { + const entry = (await Effect.runPromise(Metric.snapshot)).find( + (candidate) => + candidate.id === 'otel_contract_test_bridge_rss_bytes' && + candidate.attributes?.operation === 'gc', + ) + return (entry?.state as { readonly value?: number } | undefined)?.value } await Effect.runPromise(bridge.set({ labels: { operation: 'gc' }, value: 100 })) - expect(snapshotValue()).toBe(100) + await expect(snapshotValue()).resolves.toBe(100) await Effect.runPromise(bridge.trustedSet({ labels: { operation: 'gc' }, value: 25 })) - expect(snapshotValue()).toBe(25) + await expect(snapshotValue()).resolves.toBe(25) }) it('bridges schema-first counters to tagged Effect metrics', async () => { @@ -1018,12 +1017,13 @@ describe('OtelMetric', () => { ), ) - const pair = Metric.unsafeSnapshot(undefined).find((entry) => { - if (entry.metricKey.name !== 'otel_contract_test_bridge_counter_total') return false - const tags = Object.fromEntries(entry.metricKey.tags.map((tag) => [tag.key, tag.value])) - return tags.service === 'api' && tags.cache_hit === 'true' - }) - expect(pair?.metricState).toMatchObject({ count: 3 }) + const pair = (await Effect.runPromise(Metric.snapshot)).find( + (entry) => + entry.id === 'otel_contract_test_bridge_counter_total' && + entry.attributes?.service === 'api' && + entry.attributes?.cache_hit === 'true', + ) + expect(pair?.state).toMatchObject({ count: 3 }) }) it('bridges schema-first histograms to tagged Effect metrics', async () => { @@ -1040,12 +1040,12 @@ describe('OtelMetric', () => { await Effect.runPromise(bridge.trustedRecord({ labels: { route: 'sync' }, value: 42 })) - const pair = Metric.unsafeSnapshot(undefined).find((entry) => { - if (entry.metricKey.name !== 'otel_contract_test_bridge_duration_ms') return false - const tags = Object.fromEntries(entry.metricKey.tags.map((tag) => [tag.key, tag.value])) - return tags.route === 'sync' - }) - expect(pair?.metricState).toMatchObject({ count: 1, min: 42, max: 42, sum: 42 }) + const pair = (await Effect.runPromise(Metric.snapshot)).find( + (entry) => + entry.id === 'otel_contract_test_bridge_duration_ms' && + entry.attributes?.route === 'sync', + ) + expect(pair?.state).toMatchObject({ count: 1, min: 42, max: 42, sum: 42 }) }) it('rejects high-cardinality and unspecified-cardinality metric labels', () => { diff --git a/packages/@overeng/otel-contract/src/otel-scrape/profile-link.ts b/packages/@overeng/otel-contract/src/otel-scrape/profile-link.ts index e2e6d883ca..99e48a137c 100644 --- a/packages/@overeng/otel-contract/src/otel-scrape/profile-link.ts +++ b/packages/@overeng/otel-contract/src/otel-scrape/profile-link.ts @@ -9,9 +9,9 @@ import { } from '@overeng/content-address' /** Opaque native profile kind, e.g. `cpuprofile`, `tsc-trace`, or `cargo-timings`. */ -export const OtelScrapeProfileType = Schema.NonEmptyTrimmedString.pipe( +export const OtelScrapeProfileType = Schema.Trimmed.check(Schema.isNonEmpty()).pipe( Schema.brand('OtelScrape.ProfileType'), - Schema.annotations({ identifier: 'OtelScrape.ProfileType' }), + Schema.annotate({ identifier: 'OtelScrape.ProfileType' }), ) export type OtelScrapeProfileType = typeof OtelScrapeProfileType.Type @@ -20,12 +20,12 @@ export const OtelScrapeProfileLink = Schema.Struct({ type: OtelScrapeProfileType, digest: ContentDigest, uri: CasUri, - byteLength: Schema.NonNegativeInt, + byteLength: Schema.Natural, mediaType: MediaType, codec: Schema.optional(Codec), - schemaVersion: Schema.optional(Schema.NonNegativeInt), - ui: Schema.optional(Schema.NonEmptyTrimmedString), -}).annotations({ identifier: 'OtelScrape.ProfileLink' }) + schemaVersion: Schema.optional(Schema.Natural), + ui: Schema.optional(Schema.Trimmed.check(Schema.isNonEmpty())), +}).annotate({ identifier: 'OtelScrape.ProfileLink' }) export type OtelScrapeProfileLink = typeof OtelScrapeProfileLink.Type /** Build the VRS-defined flat profile link from the reusable content descriptor. */ diff --git a/packages/@overeng/otel-contract/src/otel-scrape/registry.gen.unit.test.ts b/packages/@overeng/otel-contract/src/otel-scrape/registry.gen.unit.test.ts index fcff1c740a..1c79545194 100644 --- a/packages/@overeng/otel-contract/src/otel-scrape/registry.gen.unit.test.ts +++ b/packages/@overeng/otel-contract/src/otel-scrape/registry.gen.unit.test.ts @@ -48,13 +48,13 @@ describe('otel-scrape generated telemetry registry', () => { for (const attributeKey of Object.values(otelScrapeAttributeKeys)) { await expect( - Effect.runPromise(Schema.decodeUnknown(OtelAttributeKey)(attributeKey)), + Effect.runPromise(Schema.decodeUnknownEffect(OtelAttributeKey)(attributeKey)), ).resolves.toBe(attributeKey) } for (const metricName of Object.values(otelScrapeMetricNames)) { await expect( - Effect.runPromise(Schema.decodeUnknown(OtelMetricName)(metricName)), + Effect.runPromise(Schema.decodeUnknownEffect(OtelMetricName)(metricName)), ).resolves.toBe(metricName) } }) diff --git a/packages/@overeng/otel-contract/src/registry.ts b/packages/@overeng/otel-contract/src/registry.ts index b125129a51..5437799c9b 100644 --- a/packages/@overeng/otel-contract/src/registry.ts +++ b/packages/@overeng/otel-contract/src/registry.ts @@ -24,7 +24,7 @@ * (referenced own attrs ∪ doc-only); foreign refs are opt-out marked (`refExternal`). * - `fragment(contract)` projects to a Layer-1 registry fragment by reading AST annotations. * - * Author-time failures raise `Schema.TaggedError`s (not plain `throw`, unlike the prototype). + * Author-time failures raise `Schema.TaggedErrorClass`s (not plain `throw`, unlike the prototype). */ import { Option, Schema } from 'effect' import * as AST from 'effect/SchemaAST' @@ -38,6 +38,8 @@ import { type OtelMetricDefinition, } from './mod.ts' +type AnyNoContext = Schema.Codec + // --------------------------------------------------------------------------- // Weaver vocabulary — MIRRORS @overeng/genie src/runtime/weaver Layer 1 (see file docstring). // --------------------------------------------------------------------------- @@ -186,9 +188,9 @@ export type RegistryFragment = { // --------------------------------------------------------------------------- /** An attribute Schema is missing the otel key annotation and/or the weaver metadata. */ -export class WeaverMissingAnnotationError extends Schema.TaggedError()( +export class WeaverMissingAnnotationError extends Schema.TaggedErrorClass()( 'WeaverMissingAnnotationError', - { key: Schema.optional(Schema.String), missing: Schema.Literal('otelKey', 'weaverMeta') }, + { key: Schema.optional(Schema.String), missing: Schema.Literals(['otelKey', 'weaverMeta']) }, ) {} /** @@ -197,13 +199,13 @@ export class WeaverMissingAnnotationError extends Schema.TaggedError()( +export class WeaverStrayNamespaceKeyError extends Schema.TaggedErrorClass()( 'WeaverStrayNamespaceKeyError', { key: Schema.String, derivedNamespace: Schema.String }, ) {} /** A contract has no own attributes, so a namespace cannot be derived. */ -export class WeaverEmptyContractError extends Schema.TaggedError()( +export class WeaverEmptyContractError extends Schema.TaggedErrorClass()( 'WeaverEmptyContractError', { memberPath: Schema.String }, ) {} @@ -213,7 +215,7 @@ export class WeaverEmptyContractError extends Schema.TaggedError()( +export class WeaverUnsupportedInstrumentError extends Schema.TaggedErrorClass()( 'WeaverUnsupportedInstrumentError', { metricName: Schema.String, instrument: Schema.String }, ) { @@ -264,13 +266,9 @@ type WeaverAttrMetaInput = { /** deep annotation read (mirrors otel-contract's own walk through Refinement/Transformation/Union). */ const getDeep = ({ id, ast }: { id: symbol; ast: AST.AST }): T | undefined => { - const here = Option.getOrUndefined(AST.getAnnotation(ast, id)) + const here = (AST.resolve(ast) as Record | undefined)?.[id] as T | undefined if (here !== undefined) return here switch (ast._tag) { - case 'Refinement': - return getDeep({ id, ast: ast.from }) - case 'Transformation': - return getDeep({ id, ast: ast.to }) ?? getDeep({ id, ast: ast.from }) case 'Union': return ast.types.map((t) => getDeep({ id, ast: t })).find((x) => x !== undefined) default: @@ -278,16 +276,16 @@ const getDeep = ({ id, ast }: { id: symbol; ast: AST.AST }): T | undefined => } } -const annotateWeaver = ({ +const annotateWeaver = ({ schema, meta, }: { schema: S meta: WeaverAttrMetaInput -}): S => schema.annotations({ [WeaverAttrAnnotationId]: meta }) as S +}): S => schema.annotate({ [WeaverAttrAnnotationId]: meta }) as S /** An attribute is an annotated Effect Schema (otel + weaver metadata carried on its AST). */ -export type Attribute = Schema.Schema.AnyNoContext +export type Attribute = AnyNoContext const keyOf = (schema: Attribute): string => { const otel = getDeep<{ key?: string }>({ id: OtelAttrAnnotationId, ast: schema.ast }) @@ -401,7 +399,7 @@ export const attr = { } & EntryMetaInput, ) => annotateWeaver({ - schema: OtelAttr.literal(o.key, ...o.values) as unknown as Schema.Schema, + schema: OtelAttr.literal(o.key, ...o.values) as unknown as Schema.Codec, meta: { brief: o.brief, stability: o.stability, @@ -519,8 +517,8 @@ type Ref = { schema: Attribute; requirement: RequirementLevel; external: boolean const buildFields = ( entries: Record, -): { fields: Record; refList: AttrRef[]; refs: Ref[] } => { - const fields: Record = {} +): { fields: Record; refList: AttrRef[]; refs: Ref[] } => { + const fields: Record = {} const refList: AttrRef[] = [] const refs: Ref[] = [] for (const [field, spec] of Object.entries(entries)) { @@ -538,12 +536,12 @@ const buildFields = ( /** * Build a `Schema.Struct` from a dynamically-assembled field record. The record is typed with - * `Schema.Struct.Field` (so it accepts `Schema.optional(...)` PropertySignatures), which widens + * `Schema.Constraint` (so it accepts `Schema.optional(...)` PropertySignatures), which widens * the result's `Context` to `unknown`; every field here is a no-context otel attribute, so the * cast back to `AnyNoContext` is sound. */ -const structOf = (fields: Record): Schema.Schema.AnyNoContext => - Schema.Struct(fields) as unknown as Schema.Schema.AnyNoContext +const structOf = (fields: Record): AnyNoContext => + Schema.Struct(fields) as unknown as AnyNoContext // --------------------------------------------------------------------------- // span / metric / operation — compose refs, derive the runtime encoder + weaver signal. @@ -560,7 +558,7 @@ const signalMetaSpread = (o: EntryMetaInput): SignalMeta => ({ export type SpanContract = { readonly kind: 'span' readonly id: string - readonly encoder: OtelAttrs + readonly encoder: OtelAttrs readonly refs: ReadonlyArray readonly signal: () => SignalDef } @@ -605,8 +603,8 @@ export const span = ( export type MetricContract = { readonly kind: 'metric' readonly id: string - readonly metric: OtelMetricDefinition - readonly encoder: OtelAttrs + readonly metric: OtelMetricDefinition + readonly encoder: OtelAttrs readonly refs: ReadonlyArray readonly signal: () => SignalDef } @@ -636,7 +634,7 @@ export const metric = ( if (o.instrument === 'updowncounter') { throw new WeaverUnsupportedInstrumentError({ metricName: o.name, instrument: o.instrument }) } - const def: OtelMetricDefinition = + const def: OtelMetricDefinition = o.instrument === 'counter' ? OtelMetric.counter({ name: o.name, @@ -695,7 +693,7 @@ export type OperationContract = { */ export const operation = < F extends Record, - L extends Record, + L extends Record, >( o: { id: string @@ -711,7 +709,7 @@ export const operation = < const { fields, refList, refs } = buildFields(o.attributes) // runtime-only label-source fields — NOT catalog refs, absent from the registry. for (const [field, schema] of Object.entries(o.labelFields ?? {})) { - fields[field] = schema as Schema.Struct.Field + fields[field] = schema as Schema.Constraint } const op = OtelOperation.define({ name: o.name, diff --git a/packages/@overeng/oxc-config/package.json.genie.ts b/packages/@overeng/oxc-config/package.json.genie.ts index 1a93882076..f585df7a7e 100644 --- a/packages/@overeng/oxc-config/package.json.genie.ts +++ b/packages/@overeng/oxc-config/package.json.genie.ts @@ -22,16 +22,7 @@ const deps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/oxc-config' }), devDependencies: { external: { - ...catalog.pick( - '@types/eslint', - '@typescript-eslint/parser', - '@typescript-eslint/rule-tester', - '@typescript-eslint/utils', - 'eslint', - 'typescript', - 'vitest', - 'oxlint-tsgolint', - ), + ...catalog.pick('@types/eslint', '@typescript-eslint/parser', '@typescript-eslint/rule-tester', '@typescript-eslint/utils', 'eslint', 'typescript', 'vitest', 'oxlint-tsgolint'), }, }, }) diff --git a/packages/@overeng/pty-effect/package.json b/packages/@overeng/pty-effect/package.json index 2fe581ef38..8fb2b223a8 100644 --- a/packages/@overeng/pty-effect/package.json +++ b/packages/@overeng/pty-effect/package.json @@ -22,16 +22,16 @@ "@overeng/otel-contract": "workspace:^" }, "devDependencies": { - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/utils-dev": "workspace:^", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vite": "8.0.16", "vitest": "4.1.9" }, "peerDependencies": { - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/pty-effect/package.json.genie.ts b/packages/@overeng/pty-effect/package.json.genie.ts index 395725c5ab..fb361dfb99 100644 --- a/packages/@overeng/pty-effect/package.json.genie.ts +++ b/packages/@overeng/pty-effect/package.json.genie.ts @@ -21,14 +21,7 @@ const workspaceDeps = catalog.compose({ devDependencies: { workspace: [utilsDevPkg], external: { - ...catalog.pick( - ...peerDepNames, - '@effect/vitest', - '@types/node', - 'typescript', - 'vite', - 'vitest', - ), + ...catalog.pick(...peerDepNames, '@effect/vitest', '@types/node', 'typescript', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/pty-effect/src/PtyError.ts b/packages/@overeng/pty-effect/src/PtyError.ts index a67c512aa1..382a214603 100644 --- a/packages/@overeng/pty-effect/src/PtyError.ts +++ b/packages/@overeng/pty-effect/src/PtyError.ts @@ -5,7 +5,7 @@ import { Schema } from 'effect' * * Follows `@effect/platform` conventions (`SystemError`, `HttpClientError`): * - `reason` discriminator for programmatic matching - * - `cause: Schema.Defect` for the upstream error (preserves name + message + * - `cause: Schema.Defect()` for the upstream error (preserves name + message * through serialization) * - Custom `get message()` for clean human-readable output in logs/traces * @@ -19,10 +19,10 @@ import { Schema } from 'effect' * - `BadName` — session name failed validation * - `Closed` — operation attempted after connection closed */ -export class PtyError extends Schema.TaggedError('@overeng/pty-effect/PtyError')( +export class PtyError extends Schema.TaggedErrorClass('@overeng/pty-effect/PtyError')( 'PtyError', { - reason: Schema.Literal( + reason: Schema.Literals([ 'SpawnFailed', 'ConnectFailed', 'WriteFailed', @@ -31,10 +31,10 @@ export class PtyError extends Schema.TaggedError('@overeng/pty-effect/ 'UnexpectedExit', 'BadName', 'Closed', - ), + ]), method: Schema.String, name: Schema.optional(Schema.String), - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { diff --git a/packages/@overeng/pty-effect/src/PtyEvent.ts b/packages/@overeng/pty-effect/src/PtyEvent.ts index caf5fdfedd..d1bf84bd83 100644 --- a/packages/@overeng/pty-effect/src/PtyEvent.ts +++ b/packages/@overeng/pty-effect/src/PtyEvent.ts @@ -2,7 +2,7 @@ import { Schema } from 'effect' import { PtyName } from './PtySpec.ts' -const Tags = Schema.Record({ key: Schema.String, value: Schema.String }) +const Tags = Schema.Record(Schema.String, Schema.String) const Base = { session: PtyName, @@ -30,7 +30,7 @@ export const NotificationEvent = Schema.Struct({ type: Schema.Literal('notification'), title: Schema.optional(Schema.String), body: Schema.optional(Schema.String), - source: Schema.optional(Schema.Literal('osc9', 'osc99', 'osc777')), + source: Schema.optional(Schema.Literals(['osc9', 'osc99', 'osc777'])), }) export type NotificationEvent = typeof NotificationEvent.Type @@ -106,7 +106,7 @@ export const SupervisorStopEvent = Schema.Struct({ export type SupervisorStopEvent = typeof SupervisorStopEvent.Type /** Union of every structured PTY event exposed by the client wrapper. */ -export const PtyEvent = Schema.Union( +export const PtyEvent = Schema.Union([ BellEvent, TitleChangeEvent, NotificationEvent, @@ -119,7 +119,7 @@ export const PtyEvent = Schema.Union( SessionFailedEvent, SupervisorStartEvent, SupervisorStopEvent, -) +]) export type PtyEvent = typeof PtyEvent.Type /** Decode a raw upstream event payload into the typed PTY event union. */ diff --git a/packages/@overeng/pty-effect/src/PtyKey.ts b/packages/@overeng/pty-effect/src/PtyKey.ts index 6967f93134..11b13dbecc 100644 --- a/packages/@overeng/pty-effect/src/PtyKey.ts +++ b/packages/@overeng/pty-effect/src/PtyKey.ts @@ -7,7 +7,7 @@ import { Schema } from 'effect' * named key the upstream adds should be added here so it's reachable from * the typed `Key` schema rather than via a free string. */ -export const NamedKey = Schema.Literal( +export const NamedKey = Schema.Literals([ 'return', 'enter', 'tab', @@ -36,7 +36,7 @@ export const NamedKey = Schema.Literal( 'f10', 'f11', 'f12', -) +]) export type NamedKey = typeof NamedKey.Type /** @@ -49,7 +49,10 @@ export type NamedKey = typeof NamedKey.Type * checked by upstream's `resolveKey` (which throws on unknown specs — * we surface that as a `WriteFailed` PtyError). */ -export const Key = Schema.String.pipe(Schema.minLength(1), Schema.brand('@overeng/pty-effect/Key')) +export const Key = Schema.String.pipe( + Schema.check(Schema.isMinLength(1)), + Schema.brand('@overeng/pty-effect/Key'), +) export type Key = typeof Key.Type /** Helper to construct a Key from a string literal at call sites. */ diff --git a/packages/@overeng/pty-effect/src/PtySession.test.ts b/packages/@overeng/pty-effect/src/PtySession.test.ts index 07f135c1e2..d91de7b365 100644 --- a/packages/@overeng/pty-effect/src/PtySession.test.ts +++ b/packages/@overeng/pty-effect/src/PtySession.test.ts @@ -275,7 +275,7 @@ describe('PtySession (server mode)', () => { }) describe('PtyError', () => { - it('is a Schema.TaggedError instance', () => { + it('is a Schema.TaggedErrorClass instance', () => { const err = new PtyError({ reason: 'Timeout', method: 'unit-test' }) expect(err._tag).toBe('PtyError') expect(err.reason).toBe('Timeout') diff --git a/packages/@overeng/pty-effect/src/PtySpawner.ts b/packages/@overeng/pty-effect/src/PtySpawner.ts index 6480f9cd36..60dd5ec127 100644 --- a/packages/@overeng/pty-effect/src/PtySpawner.ts +++ b/packages/@overeng/pty-effect/src/PtySpawner.ts @@ -12,12 +12,12 @@ import type { PtySpec } from './PtySpec.ts' * class with a `spawn` method that returns a `Scope`-bound resource. The * caller's scope owns the lifecycle. */ -export class PtySpawner extends Context.Tag('@overeng/pty-effect/PtySpawner')< +export class PtySpawner extends Context.Service< PtySpawner, { readonly spawn: (spec: PtySpec) => Effect.Effect } ->() {} +>()('@overeng/pty-effect/PtySpawner') {} /** * Default in-process layer. diff --git a/packages/@overeng/pty-effect/src/PtySpec.ts b/packages/@overeng/pty-effect/src/PtySpec.ts index cf9cccbc78..309da10f42 100644 --- a/packages/@overeng/pty-effect/src/PtySpec.ts +++ b/packages/@overeng/pty-effect/src/PtySpec.ts @@ -4,8 +4,8 @@ import { Schema } from 'effect' * Pty session names per upstream's `validateName`: `[a-zA-Z0-9._-]{1,255}`. * Branded so misuse fails at the schema layer rather than inside upstream. */ -export const PtyName = Schema.String.pipe( - Schema.pattern(/^[a-zA-Z0-9._-]{1,255}$/), +export const PtyName = Schema.String.check( + Schema.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,255}$/)), Schema.brand('@overeng/pty-effect/PtyName'), ) export type PtyName = typeof PtyName.Type @@ -27,7 +27,7 @@ export const PtySpawnSpec = Schema.Struct({ command: Schema.String, args: Schema.optional(Schema.Array(Schema.String)), cwd: Schema.optional(Schema.String), - env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })), + env: Schema.optional(Schema.Record(Schema.String, Schema.String)), size: Schema.optional(TerminalSize), }) export type PtySpawnSpec = typeof PtySpawnSpec.Type @@ -49,7 +49,7 @@ export const PtyServerSpec = Schema.Struct({ export type PtyServerSpec = typeof PtyServerSpec.Type /** Tagged union of all spec variants. */ -export const PtySpec = Schema.Union(PtySpawnSpec, PtyServerSpec) +export const PtySpec = Schema.Union([PtySpawnSpec, PtyServerSpec]) export type PtySpec = typeof PtySpec.Type /** Convenience constructors. */ diff --git a/packages/@overeng/pty-effect/src/client.ts b/packages/@overeng/pty-effect/src/client.ts index bfef91fe8f..5f6702514e 100644 --- a/packages/@overeng/pty-effect/src/client.ts +++ b/packages/@overeng/pty-effect/src/client.ts @@ -61,7 +61,7 @@ import type { Screenshot } from './Screenshot.ts' /* ───────────────────────────── specs ────────────────────────────── */ -const PtyTags = Schema.Record({ key: Schema.String, value: Schema.String }) +const PtyTags = Schema.Record(Schema.String, Schema.String) // DYNAMIC-NAME BRIDGE (SC-DQ5): a `spanName`-parameterized operation family; kept inline (the name // is not fixed per contract entry) but rebuilt from the imported catalog schemas. @@ -273,7 +273,7 @@ export interface PtyClientSession { /* ───────────────────────────── service ──────────────────────────── */ /** Effect service wrapping the detached `@myobie/pty/client` API. */ -export class PtyClient extends Context.Tag('@overeng/pty-effect/PtyClient')< +export class PtyClient extends Context.Service< PtyClient, { readonly spawnDaemon: (spec: PtyDaemonSpec) => Effect.Effect @@ -296,7 +296,7 @@ export class PtyClient extends Context.Tag('@overeng/pty-effect/PtyClient')< readonly followEvents: (spec: PtyFollowEventsSpec) => Stream.Stream readonly kill: (input: { readonly name: PtyName }) => Effect.Effect } ->() {} +>()('@overeng/pty-effect/PtyClient') {} /* ─────────────────────────── implementation ─────────────────────── */ diff --git a/packages/@overeng/react-inspector/package.json b/packages/@overeng/react-inspector/package.json index e629ac3607..7ca8cc8210 100644 --- a/packages/@overeng/react-inspector/package.json +++ b/packages/@overeng/react-inspector/package.json @@ -35,7 +35,7 @@ "@types/is-dom": "1.1.2", "@types/react": "19.2.17", "@vitejs/plugin-react": "6.0.2", - "effect": "4.0.0-beta.99", + "effect": "4.0.0-beta.102", "happy-dom": "20.10.6", "react": "19.2.7", "react-dom": "19.2.7", diff --git a/packages/@overeng/react-inspector/package.json.genie.ts b/packages/@overeng/react-inspector/package.json.genie.ts index b2abddbb75..3268e53c8c 100644 --- a/packages/@overeng/react-inspector/package.json.genie.ts +++ b/packages/@overeng/react-inspector/package.json.genie.ts @@ -26,7 +26,7 @@ const catalog = defineCatalog({ 'vite', 'vitest', ), - effect: '4.0.0-beta.99', + effect: '4.0.0-beta.102', }) const peerDepNames = ['effect', 'react'] as const @@ -39,22 +39,7 @@ const workspaceDeps = catalog.compose({ }, devDependencies: { external: { - ...catalog.pick( - ...peerDepNames, - '@storybook/react', - '@storybook/react-vite', - '@testing-library/react', - '@testing-library/user-event', - '@types/is-dom', - '@types/react', - '@vitejs/plugin-react', - 'happy-dom', - 'react-dom', - 'storybook', - 'typescript', - 'vite', - 'vitest', - ), + ...catalog.pick(...peerDepNames, '@storybook/react', '@storybook/react-vite', '@testing-library/react', '@testing-library/user-event', '@types/is-dom', '@types/react', '@vitejs/plugin-react', 'happy-dom', 'react-dom', 'storybook', 'typescript', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/restate-effect/examples/01-service.ts b/packages/@overeng/restate-effect/examples/01-service.ts index 2eeb02f31a..2fae524fda 100644 --- a/packages/@overeng/restate-effect/examples/01-service.ts +++ b/packages/@overeng/restate-effect/examples/01-service.ts @@ -13,10 +13,9 @@ import { Restate, RestateService } from '../src/mod.ts' /* ── 1. The application service the handler depends on (ordinary Effect) ──── */ /** An injected greeting prefix — satisfied from the application Layer, not per call. */ -export class Greeting extends Context.Tag('example/Greeting')< - Greeting, - { readonly prefix: string } ->() { +export class Greeting extends Context.Service()( + 'example/Greeting', +) { static readonly Default = Layer.succeed(Greeting, { prefix: 'Hello' }) } @@ -27,7 +26,7 @@ export const GreetSuccess = Schema.Struct({ message: Schema.String, id: Schema.S /** A declared business failure. It crosses the wire as a terminal error and * decodes back into THIS tagged error on the caller side (the typed boundary). */ -export class EmptyName extends Schema.TaggedError('example/EmptyName')( +export class EmptyName extends Schema.TaggedErrorClass('example/EmptyName')( 'EmptyName', {}, ) {} diff --git a/packages/@overeng/restate-effect/examples/03-workflow.ts b/packages/@overeng/restate-effect/examples/03-workflow.ts index 77790e220d..df2707761d 100644 --- a/packages/@overeng/restate-effect/examples/03-workflow.ts +++ b/packages/@overeng/restate-effect/examples/03-workflow.ts @@ -24,7 +24,7 @@ const Approval = DurablePromise.for(Decision) /* ── Typed State observable via the `status` query ── */ export const StatusState = { - status: Schema.Literal('pending', 'approved', 'rejected'), + status: Schema.Literals(['pending', 'approved', 'rejected']), } as const const Status = State.for(StatusState) diff --git a/packages/@overeng/restate-effect/examples/08-annotations.ts b/packages/@overeng/restate-effect/examples/08-annotations.ts index 88b3fc5edd..bd31780306 100644 --- a/packages/@overeng/restate-effect/examples/08-annotations.ts +++ b/packages/@overeng/restate-effect/examples/08-annotations.ts @@ -22,13 +22,13 @@ import { aesGcmRedactionLayer, Restate, RestateService } from '../src/mod.ts' /* ── Error classification: per-error status code, retryable vs terminal ────── */ /** Terminal with a 404: the caller sees status 404; Restate does not retry. */ -export class NotFound extends Schema.TaggedError('example/NotFound')('NotFound', { +export class NotFound extends Schema.TaggedErrorClass('example/NotFound')('NotFound', { id: Schema.String, }) {} export const NotFoundTerminal = Restate.terminal({ self: NotFound, errorCode: 404 }) /** Retryable: Restate re-runs the handler rather than propagating to the caller. */ -export class Throttled extends Schema.TaggedError('example/Throttled')( +export class Throttled extends Schema.TaggedErrorClass('example/Throttled')( 'Throttled', {}, ) {} diff --git a/packages/@overeng/restate-effect/examples/12-self-reschedule.ts b/packages/@overeng/restate-effect/examples/12-self-reschedule.ts index 86e9d0f7f7..582b3b112e 100644 --- a/packages/@overeng/restate-effect/examples/12-self-reschedule.ts +++ b/packages/@overeng/restate-effect/examples/12-self-reschedule.ts @@ -231,7 +231,7 @@ const defaultComposedSource: ComposedSourceBehavior = (cursor) => ({ * instance (`e.retryAfterMillis`) — exactly the Notion 429 shape (decision 0011). * The loop reads this projection (via `classifyOutcome`) to set the re-arm delay. */ -export class RateLimited extends Schema.TaggedError()('RateLimited', { +export class RateLimited extends Schema.TaggedErrorClass()('RateLimited', { retryAfterMillis: Schema.Number, }) {} const RateLimitedRetryable = Restate.retryable({ @@ -240,14 +240,14 @@ const RateLimitedRetryable = Restate.retryable({ }) /** A terminal source failure: NON-retryable, governed by the `onCycleError` policy. */ -export class SourceFailed extends Schema.TaggedError()('SourceFailed', { +export class SourceFailed extends Schema.TaggedErrorClass()('SourceFailed', { message: Schema.String, }) {} const SourceFailedTerminal = Restate.terminal({ self: SourceFailed }) /** The cycle's declared error UNION (a retryable + a terminal member), classified * per-member at the loop boundary. */ -export const ComposedError = Schema.Union(RateLimitedRetryable, SourceFailedTerminal) +export const ComposedError = Schema.Union([RateLimitedRetryable, SourceFailedTerminal]) /** The DECODED cycle-error type — the `CycleE` the typed cycle's `E` channel carries. */ export type ComposedError = Schema.Schema.Type diff --git a/packages/@overeng/restate-effect/examples/13-admin-operations.ts b/packages/@overeng/restate-effect/examples/13-admin-operations.ts index e7571da8e6..e564cd3bc2 100644 --- a/packages/@overeng/restate-effect/examples/13-admin-operations.ts +++ b/packages/@overeng/restate-effect/examples/13-admin-operations.ts @@ -34,7 +34,7 @@ import { DurablePromise, Restate, RestateObject, RestateWorkflow, State } from ' export const IncidentState = { /** The incident lifecycle the operator inspects via `QUERY object state`. */ - status: Schema.Literal('open', 'acknowledged', 'resolved'), + status: Schema.Literals(['open', 'acknowledged', 'resolved']), /** A free-form human note recorded on the last transition. */ note: Schema.String, } as const @@ -79,7 +79,7 @@ export const IncidentLive = RestateObject.implement({ export const DeliveryState = { /** Where the delivery is in its lifecycle — inspected via `INSPECT workflow state`. */ - phase: Schema.Literal('delivering', 'delivered', 'gave-up'), + phase: Schema.Literals(['delivering', 'delivered', 'gave-up']), } as const const Delivery = State.for(DeliveryState) @@ -97,7 +97,7 @@ const Release = DurablePromise.for(Schema.Struct({ go: Schema.Boolean })) * failing error (a 429's `retryAfterMillis`). Exported as the annotated schema the * contract's `error` references (the binding reads the annotation at the boundary). */ -export class DiscordUnavailable extends Schema.TaggedError( +export class DiscordUnavailable extends Schema.TaggedErrorClass( 'example/DiscordUnavailable', )('DiscordUnavailable', { retryAfterMillis: Schema.Number }) {} diff --git a/packages/@overeng/restate-effect/examples/14-http-error-classification.ts b/packages/@overeng/restate-effect/examples/14-http-error-classification.ts index abc188c533..d445400a43 100644 --- a/packages/@overeng/restate-effect/examples/14-http-error-classification.ts +++ b/packages/@overeng/restate-effect/examples/14-http-error-classification.ts @@ -1,3 +1,4 @@ +import { Effect, ParseResult, Schema } from 'effect' /** * Classifying real HTTP outcomes from an `HttpClient` call (Molty consumer recipe * #3 — the union-member classification, made concrete). A handler that calls an @@ -36,8 +37,7 @@ * upstream returning controlled statuses, and asserts each union member lands in the * right channel. Skips when no native server is available. */ -import { HttpClient, HttpClientRequest, type HttpClientResponse } from '@effect/platform' -import { Effect, ParseResult, Schema } from 'effect' +import { HttpClient, HttpClientRequest, type HttpClientResponse } from 'effect/unstable/http' import { Restate, RestateService } from '../src/mod.ts' @@ -51,17 +51,23 @@ export type Widget = Schema.Schema.Type * ════════════════════════════════════════════════════════════════════════ */ /** TERMINAL: a deterministic bad request (HTTP 400). No retry can help. */ -export class BadRequest extends Schema.TaggedError('http/BadRequest')('BadRequest', { - detail: Schema.String, -}) {} +export class BadRequest extends Schema.TaggedErrorClass('http/BadRequest')( + 'BadRequest', + { + detail: Schema.String, + }, +) {} export const BadRequestTerminal = Restate.terminal({ self: BadRequest, errorCode: 400 }) /** TERMINAL: not authorized (HTTP 403). */ -export class Forbidden extends Schema.TaggedError('http/Forbidden')('Forbidden', {}) {} +export class Forbidden extends Schema.TaggedErrorClass('http/Forbidden')( + 'Forbidden', + {}, +) {} export const ForbiddenTerminal = Restate.terminal({ self: Forbidden, errorCode: 403 }) /** TERMINAL: the resource does not exist (HTTP 404). */ -export class NotFound extends Schema.TaggedError('http/NotFound')('NotFound', { +export class NotFound extends Schema.TaggedErrorClass('http/NotFound')('NotFound', { widgetId: Schema.String, }) {} export const NotFoundTerminal = Restate.terminal({ self: NotFound, errorCode: 404 }) @@ -72,7 +78,7 @@ export const NotFoundTerminal = Restate.terminal({ self: NotFound, errorCode: 40 * terminal DOMAIN error (a 502-style "bad gateway from us to our caller"), NOT a * retryable one — distinct from a transient 5xx. */ -export class MalformedUpstream extends Schema.TaggedError( +export class MalformedUpstream extends Schema.TaggedErrorClass( 'http/MalformedUpstream', )('MalformedUpstream', { detail: Schema.String }) {} export const MalformedUpstreamTerminal = Restate.terminal({ @@ -88,7 +94,7 @@ export const MalformedUpstreamTerminal = Restate.terminal({ * header is decoded into `retryAfterMillis` and becomes the next attempt's floor * (a 5xx / timeout carries `0`, falling back to Restate's default backoff). */ -export class UpstreamUnavailable extends Schema.TaggedError( +export class UpstreamUnavailable extends Schema.TaggedErrorClass( 'http/UpstreamUnavailable', )('UpstreamUnavailable', { status: Schema.Number, @@ -163,13 +169,13 @@ const decodeWidget = ( * ════════════════════════════════════════════════════════════════════════ */ const FetchInput = Schema.Struct({ baseUrl: Schema.String, widgetId: Schema.String }) -const FetchErrorUnion = Schema.Union( +const FetchErrorUnion = Schema.Union([ BadRequestTerminal, ForbiddenTerminal, NotFoundTerminal, MalformedUpstreamTerminal, UpstreamUnavailableRetryable, -) +]) export const WidgetApi = RestateService.contract({ name: 'widget-api', diff --git a/packages/@overeng/restate-effect/package.json b/packages/@overeng/restate-effect/package.json index 5869e22da4..e19a7cdeb7 100644 --- a/packages/@overeng/restate-effect/package.json +++ b/packages/@overeng/restate-effect/package.json @@ -24,8 +24,8 @@ "@restatedev/restate-sdk-clients": "1.14.5" }, "devDependencies": { - "@effect/opentelemetry": "0.63.0", - "@effect/vitest": "0.29.0", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@opentelemetry/api": "1.9.1", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", @@ -38,23 +38,18 @@ "@overeng/utils-dev": "workspace:^", "@restatedev/restate-sdk-opentelemetry": "1.14.5", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "@effect/cluster": "^0.59.0", - "@effect/experimental": "^0.60.0", - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/rpc": "^0.75.1", - "@effect/workflow": "^0.18.2", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@opentelemetry/api": "^1.9.1", "@opentelemetry/sdk-metrics": "^2.8.0", "@playwright/test": "^1.61.0", "@restatedev/restate-sdk-opentelemetry": "^1.14.5", - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "$genie": { "source": "package.json.genie.ts", diff --git a/packages/@overeng/restate-effect/package.json.genie.ts b/packages/@overeng/restate-effect/package.json.genie.ts index 1ab3099b17..b3de313568 100644 --- a/packages/@overeng/restate-effect/package.json.genie.ts +++ b/packages/@overeng/restate-effect/package.json.genie.ts @@ -46,15 +46,7 @@ const workspaceDeps = catalog.compose({ * locally. */ workspace: [utilsDevPkg, utilsPkg], external: { - ...catalog.pick( - ...peerDepNames, - ...otelPeerDepNames, - ...otelSdkDeps, - '@effect/vitest', - '@types/node', - 'typescript', - 'vitest', - ), + ...catalog.pick(...peerDepNames, ...otelPeerDepNames, ...otelSdkDeps, '@effect/vitest', '@types/node', 'typescript', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/restate-effect/src/admin/admin.ts b/packages/@overeng/restate-effect/src/admin/admin.ts index 0f224d1124..029f8c47e6 100644 --- a/packages/@overeng/restate-effect/src/admin/admin.ts +++ b/packages/@overeng/restate-effect/src/admin/admin.ts @@ -259,10 +259,9 @@ const toClientConfig = (opts: { * `RestateIngress` Tag/layer pattern — but points at the admin URL, NOT the * ingress URL, and carries the admin-API trust boundary (see the module doc). */ -export class RestateAdmin extends Context.Tag('@overeng/restate-effect/RestateAdmin')< - RestateAdmin, - RestateAdminService ->() { +export class RestateAdmin extends Context.Service()( + '@overeng/restate-effect/RestateAdmin', +) { /** * Build a `RestateAdmin` layer bound to a `restate-server` ADMIN URL (the * PRIMITIVE form). For a SECURED / Restate Cloud admin endpoint, pass `apiKey` diff --git a/packages/@overeng/restate-effect/src/authoring/RestateContext.ts b/packages/@overeng/restate-effect/src/authoring/RestateContext.ts index 584df42bd4..4cbdd5e40f 100644 --- a/packages/@overeng/restate-effect/src/authoring/RestateContext.ts +++ b/packages/@overeng/restate-effect/src/authoring/RestateContext.ts @@ -18,10 +18,9 @@ import { internalSerde } from '../schema/Serde.ts' * long-lived application `Layer`. Durable combinators carry `RestateContext` * in their `R` and reach the raw SDK context via `yield* RestateContext`. */ -export class RestateContext extends Context.Tag('@overeng/restate-effect/RestateContext')< - RestateContext, - restate.Context ->() {} +export class RestateContext extends Context.Service()( + '@overeng/restate-effect/RestateContext', +) {} /* ── capability markers (flat, independent — see decision 0002) ──────────── */ /* @@ -40,28 +39,27 @@ export class RestateContext extends Context.Tag('@overeng/restate-effect/Restate type CapabilityMarker = { readonly [K in Brand_]: never } /** Permits `State.get` / `State.stateKeys`. Provided to object/workflow handlers. */ -export class StateRead extends Context.Tag('@overeng/restate-effect/StateRead')< +export class StateRead extends Context.Service< StateRead, CapabilityMarker<'requires a State-readable (object/workflow) handler'> ->() {} +>()('@overeng/restate-effect/StateRead') {} /** Permits `State.set` / `State.clear` / `State.clearAll`. Exclusive object / workflow `run` only. */ -export class StateWrite extends Context.Tag('@overeng/restate-effect/StateWrite')< +export class StateWrite extends Context.Service< StateWrite, CapabilityMarker<'requires a write-enabled (exclusive object / workflow run) handler'> ->() {} +>()('@overeng/restate-effect/StateWrite') {} /** Permits `DurablePromise.*`. Workflow handlers only. */ -export class DurablePromise extends Context.Tag('@overeng/restate-effect/DurablePromise')< +export class DurablePromise extends Context.Service< DurablePromise, CapabilityMarker<'requires a Workflow handler (durable promises)'> ->() {} +>()('@overeng/restate-effect/DurablePromise') {} /** Permits the `ctx.key` accessor. Object / workflow handlers (all keyed). */ -export class ObjectKey extends Context.Tag('@overeng/restate-effect/ObjectKey')< - ObjectKey, - { readonly key: string } ->() {} +export class ObjectKey extends Context.Service()( + '@overeng/restate-effect/ObjectKey', +) {} /** * The full set of durable capabilities `Restate.run` scrubs from its inner @@ -95,7 +93,7 @@ export type StateValueType = * `PropertySignature` AST (`.type` is the `value | undefined` union) with the * `undefined` member STRIPPED — a SET value is always the present `T` (the * "unset → undefined" case never reaches the serde, the State combinator returns - * `undefined` directly), and keeping `undefined` would break `JSONSchema.make`. + * `undefined` directly), and keeping `undefined` would break `JsonSchema.make`. */ export const normalizeStateSchema = ( field: Schema.Schema | Schema.PropertySignature.All, diff --git a/packages/@overeng/restate-effect/src/authoring/capability-inference.types.ts b/packages/@overeng/restate-effect/src/authoring/capability-inference.types.ts index 88f77e72f6..616d695b81 100644 --- a/packages/@overeng/restate-effect/src/authoring/capability-inference.types.ts +++ b/packages/@overeng/restate-effect/src/authoring/capability-inference.types.ts @@ -37,7 +37,7 @@ type Equals = const GreetInput = Schema.Struct({ name: Schema.String }) const GreetSuccess = Schema.Struct({ message: Schema.String, id: Schema.String }) -class EmptyName extends Schema.TaggedError('test/EmptyName')('EmptyName', {}) {} +class EmptyName extends Schema.TaggedErrorClass('test/EmptyName')('EmptyName', {}) {} const Greeter = RestateService.contract({ name: 'greeter', @@ -230,14 +230,14 @@ const Approval = DurablePromise.for(Decision) const Approve = RestateWorkflow.contract({ name: 'approve', def: { - state: { status: Schema.Literal('pending', 'approved', 'rejected') }, + state: { status: Schema.Literals(['pending', 'approved', 'rejected']) }, payload: { input: Schema.String, success: Schema.Boolean }, signals: { approve: { input: Decision, success: Schema.Void } }, queries: { status: { input: Schema.Void, success: Schema.String } }, }, }) const ApproveState = State.for({ - status: Schema.Literal('pending', 'approved', 'rejected'), + status: Schema.Literals(['pending', 'approved', 'rejected']), }) /* POSITIVE: `run` writes State + awaits a durable promise; the signal resolves it diff --git a/packages/@overeng/restate-effect/src/authoring/concurrency.integration.test.ts b/packages/@overeng/restate-effect/src/authoring/concurrency.integration.test.ts index ee4517ed45..322ef8535d 100644 --- a/packages/@overeng/restate-effect/src/authoring/concurrency.integration.test.ts +++ b/packages/@overeng/restate-effect/src/authoring/concurrency.integration.test.ts @@ -25,7 +25,7 @@ import { RestateTestHarness, serverAvailable } from '../testing/testing.ts' const Concurrent = RestateService.contract({ name: 'concurrency-demo', handlers: { - all: { input: Schema.Void, success: Schema.Tuple(Schema.String, Schema.String) }, + all: { input: Schema.Void, success: Schema.Tuple([Schema.String, Schema.String]) }, race: { input: Schema.Void, success: Schema.String }, }, }) diff --git a/packages/@overeng/restate-effect/src/authoring/workflow.integration.test.ts b/packages/@overeng/restate-effect/src/authoring/workflow.integration.test.ts index d584a5711f..ac005f14f9 100644 --- a/packages/@overeng/restate-effect/src/authoring/workflow.integration.test.ts +++ b/packages/@overeng/restate-effect/src/authoring/workflow.integration.test.ts @@ -26,7 +26,7 @@ import { serverAvailable, withRestateServer } from '../testing/testing.ts' const Decision = Schema.Struct({ approved: Schema.Boolean }) const Approval = DurablePromise.for(Decision) -const StatusState = { status: Schema.Literal('pending', 'approved', 'rejected') } as const +const StatusState = { status: Schema.Literals(['pending', 'approved', 'rejected']) } as const const Status = State.for(StatusState) const ApprovalWf = RestateWorkflow.contract({ diff --git a/packages/@overeng/restate-effect/src/clients/Client.ts b/packages/@overeng/restate-effect/src/clients/Client.ts index 6eace545da..5f03859d54 100644 --- a/packages/@overeng/restate-effect/src/clients/Client.ts +++ b/packages/@overeng/restate-effect/src/clients/Client.ts @@ -58,10 +58,9 @@ export interface RestateIngressService { * env-driven `RestateIngress.layerConfig`) and `yield* RestateIngress` (or thread * `call` / `callTyped`, which require it in `R`). */ -export class RestateIngress extends Context.Tag('@overeng/restate-effect/RestateIngress')< - RestateIngress, - RestateIngressService ->() { +export class RestateIngress extends Context.Service()( + '@overeng/restate-effect/RestateIngress', +) { /** * Build a `RestateIngress` layer bound to a `restate-server` ingress URL. The * PRIMITIVE form (a thin wrapper over `clients.connect`). diff --git a/packages/@overeng/restate-effect/src/clients/contract-policy.integration.test.ts b/packages/@overeng/restate-effect/src/clients/contract-policy.integration.test.ts index 408b72513d..c38964ccc0 100644 --- a/packages/@overeng/restate-effect/src/clients/contract-policy.integration.test.ts +++ b/packages/@overeng/restate-effect/src/clients/contract-policy.integration.test.ts @@ -71,7 +71,7 @@ const EchoLive = RestateService.implement({ /* ── (3) an Object: sensitive field + idempotency-key field + typed terminal error ── */ -class Denied extends Schema.TaggedError()('Denied', { reason: Schema.String }) {} +class Denied extends Schema.TaggedErrorClass()('Denied', { reason: Schema.String }) {} const VaultState = { token: Schema.String } as const const VaultS = State.for(VaultState) @@ -211,7 +211,7 @@ describe.skipIf(!serverAvailable)('contract-invocation policy at the public entr /* Raw wire: the sensitive field is ciphertext, never the plaintext. */ expect(body).not.toContain(SECRET) const wire = yield* Schema.decode( - Schema.parseJson(Schema.Struct({ token: Schema.String })), + Schema.fromJsonString(Schema.Struct({ token: Schema.String })), )(body) expect(wire.token).not.toBe(SECRET) /* The typed `call` over the SAME handler decrypts the response to plaintext. */ diff --git a/packages/@overeng/restate-effect/src/endpoint/Endpoint.ts b/packages/@overeng/restate-effect/src/endpoint/Endpoint.ts index de4a6b3698..b4ccf411d5 100644 --- a/packages/@overeng/restate-effect/src/endpoint/Endpoint.ts +++ b/packages/@overeng/restate-effect/src/endpoint/Endpoint.ts @@ -230,7 +230,7 @@ const mapRetention = ({ retention: RetentionOptions includeWorkflow: boolean }): Record => { - const toMillis = (d: Duration.DurationInput): number => Duration.toMillis(Duration.decode(d)) + const toMillis = (d: Duration.Input): number => Duration.toMillis(Duration.fromInputUnsafe(d)) return { ...(retention.idempotency !== undefined ? { idempotencyRetention: toMillis(retention.idempotency) } @@ -745,10 +745,9 @@ export interface EndpointServer { } /** Context service carrying the actual address of a scoped endpoint server. */ -export class BoundEndpoint extends Context.Tag('@overeng/restate-effect/BoundEndpoint')< - BoundEndpoint, - EndpointServer ->() {} +export class BoundEndpoint extends Context.Service()( + '@overeng/restate-effect/BoundEndpoint', +) {} /* eslint-disable @typescript-eslint/no-explicit-any -- the services-tuple AppR extractor */ /** diff --git a/packages/@overeng/restate-effect/src/endpoint/restate-effect.integration.test.ts b/packages/@overeng/restate-effect/src/endpoint/restate-effect.integration.test.ts index 8cf2cb8efd..6e17b5cf82 100644 --- a/packages/@overeng/restate-effect/src/endpoint/restate-effect.integration.test.ts +++ b/packages/@overeng/restate-effect/src/endpoint/restate-effect.integration.test.ts @@ -16,11 +16,11 @@ import { serverAvailable, withRestateServer } from '../testing/testing.ts' /* ── demo app: an injected Effect service + a greeter Restate service ── */ -class Greeting extends Context.Tag('test/Greeting')() { +class Greeting extends Context.Service()('test/Greeting') { static readonly Default = Layer.succeed(Greeting, { prefix: 'Hello' }) } -class EmptyName extends Schema.TaggedError('test/EmptyName')('EmptyName', {}) {} +class EmptyName extends Schema.TaggedErrorClass('test/EmptyName')('EmptyName', {}) {} const GreetInput = Schema.Struct({ name: Schema.String }) const GreetSuccess = Schema.Struct({ message: Schema.String, id: Schema.String }) diff --git a/packages/@overeng/restate-effect/src/error/Boundary.ts b/packages/@overeng/restate-effect/src/error/Boundary.ts index 74c3174b48..70f35b83a3 100644 --- a/packages/@overeng/restate-effect/src/error/Boundary.ts +++ b/packages/@overeng/restate-effect/src/error/Boundary.ts @@ -104,7 +104,7 @@ export const classifyOutcome = ({ return { _tag: 'suspended', thrown: suspensionDefect.value } } - const failure = Cause.failureOption(cause) + const failure = Cause.findErrorOption(cause) if (failure._tag === 'Some') { const error = failure.value /* A Restate suspension is never a real failure — never terminalize it. */ diff --git a/packages/@overeng/restate-effect/src/error/error-transport.test.ts b/packages/@overeng/restate-effect/src/error/error-transport.test.ts index b11c914aa8..ab0eb66249 100644 --- a/packages/@overeng/restate-effect/src/error/error-transport.test.ts +++ b/packages/@overeng/restate-effect/src/error/error-transport.test.ts @@ -1,6 +1,6 @@ /** * Server-free contract-layer test of the error-transport round-trip (docs/vrs/04-error-boundary/spec.md §1, - * docs/vrs/09-testing/spec.md §3): a domain `Schema.TaggedError` → `toTerminal` (per-error errorCode, + * docs/vrs/09-testing/spec.md §3): a domain `Schema.TaggedErrorClass` → `toTerminal` (per-error errorCode, * `_tag` in the message body) → a simulated ingress `HttpCallError` → the * `decodeTerminalError` helper → back to the typed tagged error, so a caller * `catchTag`s it. No native server involved. @@ -17,7 +17,7 @@ import { RestateError } from '../schema/RestateError.ts' import { toTerminal } from './Boundary.ts' /* A domain error annotated terminal with a custom errorCode (404). */ -class NotFound extends Schema.TaggedError('test/NotFound')('NotFound', { +class NotFound extends Schema.TaggedErrorClass('test/NotFound')('NotFound', { id: Schema.String, }) {} const NotFoundSchema = Restate.terminal({ self: Schema.asSchema(NotFound), errorCode: 404 }) @@ -104,14 +104,14 @@ describe('error transport (contract layer, server-free)', () => { ) expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit) === true) { - const failure = Cause.failureOption(exit.cause) + const failure = Cause.findErrorOption(exit.cause) expect(failure._tag).toBe('Some') if (failure._tag === 'Some') expect(failure.value).toBeInstanceOf(RestateError) } }) it('a retryable-annotated error throws RetryableError instead of terminalizing', () => { - class Throttled extends Schema.TaggedError('test/Throttled')('Throttled', {}) {} + class Throttled extends Schema.TaggedErrorClass('test/Throttled')('Throttled', {}) {} const ThrottledSchema = Restate.retryable({ self: Schema.asSchema(Throttled) }) const out = toTerminal({ cause: Cause.fail(new Throttled()), errorSchema: ThrottledSchema }) expect(out).toBeInstanceOf(restate.RetryableError) @@ -121,9 +121,12 @@ describe('error transport (contract layer, server-free)', () => { it('retryAfter is projected from the ACTUAL error instance (#3)', () => { /* A 429-style error carrying its own retry floor; the projection reads it off * the very instance that failed, not a static literal. */ - class RateLimited extends Schema.TaggedError('test/RateLimited')('RateLimited', { - retryAfterMillis: Schema.Number, - }) {} + class RateLimited extends Schema.TaggedErrorClass('test/RateLimited')( + 'RateLimited', + { + retryAfterMillis: Schema.Number, + }, + ) {} const RateLimitedSchema = Restate.retryable({ self: Schema.asSchema(RateLimited), retryAfter: (e) => e.retryAfterMillis, @@ -137,14 +140,14 @@ describe('error transport (contract layer, server-free)', () => { }) it('retryAfter accepts a static Duration shorthand', () => { - class Slow extends Schema.TaggedError('test/Slow')('Slow', {}) {} + class Slow extends Schema.TaggedErrorClass('test/Slow')('Slow', {}) {} const SlowSchema = Restate.retryable({ self: Schema.asSchema(Slow), retryAfter: '2 seconds' }) const out = toTerminal({ cause: Cause.fail(new Slow()), errorSchema: SlowSchema }) expect((out as restate.RetryableError).retryAfter).toBe(2_000) }) it('a projection returning undefined falls back to default backoff (no floor)', () => { - class Maybe extends Schema.TaggedError('test/Maybe')('Maybe', { + class Maybe extends Schema.TaggedErrorClass('test/Maybe')('Maybe', { after: Schema.optional(Schema.Number), }) {} const MaybeSchema = Restate.retryable({ @@ -162,17 +165,20 @@ describe('error transport (contract layer, server-free)', () => { * union node, so the boundary must resolve the matching member for the actual * failing error before reading the class — else the retryable member is silently * mis-classified as terminal (the pollLoop compose blocker). */ - class RateLimited extends Schema.TaggedError('test/RateLimited2')('RateLimited', { - retryAfterMillis: Schema.Number, - }) {} - class Gone extends Schema.TaggedError('test/Gone')('Gone', { id: Schema.String }) {} - const UnionSchema = Schema.Union( + class RateLimited extends Schema.TaggedErrorClass('test/RateLimited2')( + 'RateLimited', + { + retryAfterMillis: Schema.Number, + }, + ) {} + class Gone extends Schema.TaggedErrorClass('test/Gone')('Gone', { id: Schema.String }) {} + const UnionSchema = Schema.Union([ Restate.retryable({ self: Schema.asSchema(RateLimited), retryAfter: (e) => e.retryAfterMillis, }), Restate.terminal({ self: Schema.asSchema(Gone), errorCode: 404 }), - ) + ]) /* The RETRYABLE member → a RetryableError honoring its projected retryAfter. */ const retry = toTerminal({ @@ -198,7 +204,7 @@ describe('error transport (contract layer, server-free)', () => { /* `Registry.lookup` declares `NotFound`; a foreign tagged error must not be * silently encoded into a terminal body — it is error-classification drift and * surfaces as a defect (the squashed cause) so the SDK retries / the bug shows. */ - class Foreign extends Schema.TaggedError('test/Foreign')('Foreign', { + class Foreign extends Schema.TaggedErrorClass('test/Foreign')('Foreign', { whatever: Schema.String, }) {} const out = toTerminal({ @@ -259,7 +265,7 @@ describe('error transport wire baselines (cross-major invariant)', () => { expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit) === true) { - const failure = Cause.failureOption(exit.cause) + const failure = Cause.findErrorOption(exit.cause) expect(failure._tag).toBe('Some') if (failure._tag === 'Some') { expect(failure.value).toBeInstanceOf(RestateError) diff --git a/packages/@overeng/restate-effect/src/error/http-error-classification.integration.test.ts b/packages/@overeng/restate-effect/src/error/http-error-classification.integration.test.ts index 55fd2ac79e..e103d601ef 100644 --- a/packages/@overeng/restate-effect/src/error/http-error-classification.integration.test.ts +++ b/packages/@overeng/restate-effect/src/error/http-error-classification.integration.test.ts @@ -19,9 +19,9 @@ import { createServer, type Server } from 'node:http' import type { AddressInfo } from 'node:net' -import { FetchHttpClient } from '@effect/platform' import { it } from '@effect/vitest' import { Effect } from 'effect' +import { FetchHttpClient } from 'effect/unstable/http' import { afterAll, beforeAll, describe, expect } from 'vitest' import { diff --git a/packages/@overeng/restate-effect/src/error/retry-policy.integration.test.ts b/packages/@overeng/restate-effect/src/error/retry-policy.integration.test.ts index 024c02f2f6..ed8973807c 100644 --- a/packages/@overeng/restate-effect/src/error/retry-policy.integration.test.ts +++ b/packages/@overeng/restate-effect/src/error/retry-policy.integration.test.ts @@ -25,7 +25,7 @@ import { liveSleep, RestateTestHarness, serverAvailable } from '../testing/testi /* A retryable domain error with a SHORT `retryAfter` floor projected from the error * instance (so the retries elapse quickly + the projection path is exercised). */ -class Throttled extends Schema.TaggedError('retry/Throttled')('Throttled', { +class Throttled extends Schema.TaggedErrorClass('retry/Throttled')('Throttled', { retryAfterMillis: Schema.Number, }) {} const ThrottledRetryable = Restate.retryable({ diff --git a/packages/@overeng/restate-effect/src/error/suspension.integration.test.ts b/packages/@overeng/restate-effect/src/error/suspension.integration.test.ts index 248e42ee03..82618b281b 100644 --- a/packages/@overeng/restate-effect/src/error/suspension.integration.test.ts +++ b/packages/@overeng/restate-effect/src/error/suspension.integration.test.ts @@ -137,7 +137,7 @@ const PromiseWfLive = RestateWorkflow.implement({ /* A declared error with NO `retryable`/`terminal` annotation classifies as TERMINAL * (the boundary default), so the callee's `boom` fails the invocation terminally. */ -class CalleeRejected extends Schema.TaggedError()('CalleeRejected', {}) {} +class CalleeRejected extends Schema.TaggedErrorClass()('CalleeRejected', {}) {} const FailingCallee = RestateService.contract({ name: 'suspend-call-callee', diff --git a/packages/@overeng/restate-effect/src/observability/observability.test.ts b/packages/@overeng/restate-effect/src/observability/observability.test.ts index efac41713c..01102f7e87 100644 --- a/packages/@overeng/restate-effect/src/observability/observability.test.ts +++ b/packages/@overeng/restate-effect/src/observability/observability.test.ts @@ -18,7 +18,7 @@ * emission does NOT increment (no double-count across attempts). */ import * as Resource from '@effect/opentelemetry/Resource' -import * as EffectTracer from '@effect/opentelemetry/Tracer' +import * as EffectTracer from '@effect/opentelemetry/OtelTracer' import { context, trace } from '@opentelemetry/api' import { AggregationTemporality, diff --git a/packages/@overeng/restate-effect/src/observability/otel.test.ts b/packages/@overeng/restate-effect/src/observability/otel.test.ts index 5b265aa3d9..9ec52623be 100644 --- a/packages/@overeng/restate-effect/src/observability/otel.test.ts +++ b/packages/@overeng/restate-effect/src/observability/otel.test.ts @@ -19,7 +19,7 @@ * `Effect.withSpan` and the hook emit into the one in-memory exporter. */ import * as Resource from '@effect/opentelemetry/Resource' -import * as EffectTracer from '@effect/opentelemetry/Tracer' +import * as EffectTracer from '@effect/opentelemetry/OtelTracer' import { context, type SpanContext, trace } from '@opentelemetry/api' import { InMemorySpanExporter, diff --git a/packages/@overeng/restate-effect/src/observability/otel.ts b/packages/@overeng/restate-effect/src/observability/otel.ts index 9535d119c9..a5343d3206 100644 --- a/packages/@overeng/restate-effect/src/observability/otel.ts +++ b/packages/@overeng/restate-effect/src/observability/otel.ts @@ -33,9 +33,9 @@ * reads a version-fragile internal SDK flag — prefer `Restate.run`. */ -import * as EffectMetrics from '@effect/opentelemetry/Metrics' +import * as EffectMetrics from '@effect/opentelemetry/OtelMetrics' import * as Resource from '@effect/opentelemetry/Resource' -import * as EffectTracer from '@effect/opentelemetry/Tracer' +import * as EffectTracer from '@effect/opentelemetry/OtelTracer' import { type Span, trace } from '@opentelemetry/api' import { resourceFromAttributes } from '@opentelemetry/resources' import type { MetricReader, PushMetricExporter } from '@opentelemetry/sdk-metrics' diff --git a/packages/@overeng/restate-effect/src/runtime/Runtime.ts b/packages/@overeng/restate-effect/src/runtime/Runtime.ts index 3181308e61..e71940bc1c 100644 --- a/packages/@overeng/restate-effect/src/runtime/Runtime.ts +++ b/packages/@overeng/restate-effect/src/runtime/Runtime.ts @@ -194,7 +194,7 @@ const makeConsoleLogger = (ctx: restate.Context): Logger.Logger = * handler, so it is unaffected (it keeps the process default logger). */ export const loggerLayer = (ctx: restate.Context): Layer.Layer => - Logger.replace(Logger.defaultLogger, makeConsoleLogger(ctx)) + Logger.layer([makeConsoleLogger(ctx)]) /* ── cancellation ↔ interruption bridge (R31, docs/vrs/04-error-boundary/spec.md §2) ──────────────── */ diff --git a/packages/@overeng/restate-effect/src/runtime/cancellation.integration.test.ts b/packages/@overeng/restate-effect/src/runtime/cancellation.integration.test.ts index f5130dca50..3d42f4dd33 100644 --- a/packages/@overeng/restate-effect/src/runtime/cancellation.integration.test.ts +++ b/packages/@overeng/restate-effect/src/runtime/cancellation.integration.test.ts @@ -38,7 +38,7 @@ const acquireGate = new Map>() /* The handler never reached its acquire gate within the deadline — the test setup * raced the cancel ahead of the suspend point. A typed failure (not a global * `Error`) so the `timeoutFail` failure channel stays type-safe. */ -class HandlerNeverAcquired extends Schema.TaggedError()( +class HandlerNeverAcquired extends Schema.TaggedErrorClass()( 'HandlerNeverAcquired', {}, ) {} diff --git a/packages/@overeng/restate-effect/src/scheduling/Scheduled.ts b/packages/@overeng/restate-effect/src/scheduling/Scheduled.ts index fa0e1fe16f..f4a982bd22 100644 --- a/packages/@overeng/restate-effect/src/scheduling/Scheduled.ts +++ b/packages/@overeng/restate-effect/src/scheduling/Scheduled.ts @@ -148,7 +148,7 @@ export type WakePayload = Schema.Schema.Type * `cycle` body reads/writes via the typed `state` it is handed. * ════════════════════════════════════════════════════════════════════════ */ -const StatusSchema = Schema.Literal('idle', 'running', 'stopped', 'failed', 'completed') +const StatusSchema = Schema.Literals(['idle', 'running', 'stopped', 'failed', 'completed']) /** The loop control-plane state, persisted in the Object's typed K/V State. */ const ControlState = { @@ -266,7 +266,7 @@ export interface ScheduledConfig Duration.DurationInput | undefined) + | Duration.Input + | ((error: unknown) => Duration.Input | undefined) /** - * The error-boundary classification for a domain `Schema.TaggedError`, read by + * The error-boundary classification for a domain `Schema.TaggedErrorClass`, read by * `toTerminal`. `terminal` (the default) maps to a non-retryable * `TerminalError` with the given `errorCode` (default 500); `retryable` maps to * a non-terminal throw so Restate retries, with an optional `retryAfter` floor @@ -44,7 +44,7 @@ export type ErrorClass = | { readonly _tag: 'terminal'; readonly errorCode: number } | { readonly _tag: 'retryable'; readonly retryAfter?: RetryAfter } -/** Override the default `application/json` content type / `JSONSchema.make`. */ +/** Override the default `application/json` content type / `JsonSchema.make`. */ export interface SerdeOptions { readonly contentType?: string readonly jsonSchema?: object @@ -53,14 +53,14 @@ export interface SerdeOptions { /** * Retention/visibility facts carried on a contract or handler value schema and * mapped to the SDK service/handler options at `materialize` (decision 0011, - * docs/vrs/01-authoring/spec.md §4.1). Durations are `Duration.DurationInput` (decoded to millis at the boundary). + * docs/vrs/01-authoring/spec.md §4.1). Durations are `Duration.Input` (decoded to millis at the boundary). * `journal`/`idempotency` apply to any construct; `workflow` only to a Workflow * (it is dropped for Services/Objects). */ export interface RetentionOptions { - readonly idempotency?: Duration.DurationInput - readonly journal?: Duration.DurationInput - readonly workflow?: Duration.DurationInput + readonly idempotency?: Duration.Input + readonly journal?: Duration.Input + readonly workflow?: Duration.Input } /* ── symbol ids ─────────────────────────────────────────────────────────── */ @@ -85,7 +85,7 @@ export const SensitiveId: unique symbol = id('sensitive') as typeof SensitiveId const annotate = (annotationId: symbol) => ({ self, value }: { self: S; value: Value }): S => - self.annotations({ [annotationId]: value }) as S + self.annotate({ [annotationId]: value }) as S /** * The `Restate` Schema-annotation namespace. Each applier attaches a @@ -93,7 +93,7 @@ const annotate = */ export const Restate = { /** - * Mark a `Schema.TaggedError` terminal (non-retryable) with an explicit + * Mark a `Schema.TaggedErrorClass` terminal (non-retryable) with an explicit * `errorCode` (e.g. 404/409). Default classification — `toTerminal` falls * back to `terminal` + 500 when no annotation is present. */ @@ -110,7 +110,7 @@ export const Restate = { }), /** - * Mark a `Schema.TaggedError` retryable: `toTerminal` throws it non-terminally + * Mark a `Schema.TaggedErrorClass` retryable: `toTerminal` throws it non-terminally * so Restate retries, honoring an optional `retryAfter` floor. `retryAfter` is * either a STATIC value (literal shorthand, e.g. `'30 seconds'`) or an INSTANCE * PROJECTION `(error) => DurationInput | undefined` read from the actual failing @@ -124,8 +124,8 @@ export const Restate = { }: { self: S retryAfter?: - | Duration.DurationInput - | ((error: Schema.Schema.Type) => Duration.DurationInput | undefined) + | Duration.Input + | ((error: Schema.Schema.Type) => Duration.Input | undefined) }): S => annotate(ErrorClassId)({ self, @@ -188,7 +188,7 @@ export const Restate = { /** Read the error classification from a schema's AST (`None` if unannotated). */ export const readErrorClass = (ast: SchemaAST.AST): Option.Option => - SchemaAST.getAnnotation(ErrorClassId)(ast) + SchemaAST.resolveAt(ErrorClassId)(ast) /** * Resolve a `retryable` classification's `retryAfter` floor against the ACTUAL @@ -211,7 +211,7 @@ export const readRetryAfterMillis = ({ typeof retryAfter === 'function' ? safeProject({ project: retryAfter, error }) : retryAfter if (value === undefined) return undefined try { - return Duration.toMillis(Duration.decode(value)) + return Duration.toMillis(Duration.fromInputUnsafe(value)) } catch { return undefined } @@ -221,9 +221,9 @@ const safeProject = ({ project, error, }: { - project: (error: unknown) => Duration.DurationInput | undefined + project: (error: unknown) => Duration.Input | undefined error: unknown -}): Duration.DurationInput | undefined => { +}): Duration.Input | undefined => { try { return project(error) } catch { @@ -233,11 +233,11 @@ const safeProject = ({ /** Read the serde options from a schema's AST (`None` if unannotated). */ export const readSerdeOptions = (ast: SchemaAST.AST): Option.Option => - SchemaAST.getAnnotation(SerdeId)(ast) + SchemaAST.resolveAt(SerdeId)(ast) /** Read the retention options from a schema's AST (`None` if unannotated). */ export const readRetention = (ast: SchemaAST.AST): Option.Option => - SchemaAST.getAnnotation(RetentionId)(ast) + SchemaAST.resolveAt(RetentionId)(ast) /** * Find the name of the input-struct field carrying the `idempotencyKey` @@ -250,7 +250,7 @@ export const findIdempotencyKeyField = (ast: SchemaAST.AST): Option.Option(IdempotencyKeyId)(prop.type)) === true) { + if (Option.isSome(SchemaAST.resolveAt(IdempotencyKeyId)(prop.type)) === true) { return Option.some(prop.name) } } @@ -291,7 +291,7 @@ const allIdempotencyKeyFields = (ast: SchemaAST.AST): ReadonlyArray => { const fields: string[] = [] for (const prop of ast.propertySignatures) { if (typeof prop.name !== 'string') continue - if (Option.isSome(SchemaAST.getAnnotation(IdempotencyKeyId)(prop.type)) === true) { + if (Option.isSome(SchemaAST.resolveAt(IdempotencyKeyId)(prop.type)) === true) { fields.push(prop.name) } } @@ -310,8 +310,8 @@ const structLevelFieldAnnotation = ( ): { readonly idempotencyKey: boolean; readonly sensitive: boolean } => { if (ast._tag !== 'TypeLiteral') return { idempotencyKey: false, sensitive: false } return { - idempotencyKey: Option.isSome(SchemaAST.getAnnotation(IdempotencyKeyId)(ast)), - sensitive: Option.isSome(SchemaAST.getAnnotation(SensitiveId)(ast)), + idempotencyKey: Option.isSome(SchemaAST.resolveAt(IdempotencyKeyId)(ast)), + sensitive: Option.isSome(SchemaAST.resolveAt(SensitiveId)(ast)), } } diff --git a/packages/@overeng/restate-effect/src/schema/Redaction.ts b/packages/@overeng/restate-effect/src/schema/Redaction.ts index 4182f0c6d2..61a33469fb 100644 --- a/packages/@overeng/restate-effect/src/schema/Redaction.ts +++ b/packages/@overeng/restate-effect/src/schema/Redaction.ts @@ -44,10 +44,9 @@ export interface RedactionCipher { * `RestateRedaction` is provided, encode/decode FAILS with a clear error rather * than silently passing plaintext (see {@link withRedaction}). */ -export class RestateRedaction extends Context.Tag('@overeng/restate-effect/RestateRedaction')< - RestateRedaction, - RedactionCipher ->() {} +export class RestateRedaction extends Context.Service()( + '@overeng/restate-effect/RestateRedaction', +) {} /* ── reference AES-256-GCM cipher (node:crypto) ──────────────────────────── */ @@ -109,7 +108,7 @@ export const findSensitiveFields = (ast: SchemaAST.AST): ReadonlyArray = const fields: string[] = [] for (const prop of ast.propertySignatures) { if (typeof prop.name !== 'string') continue - if (Option.isSome(SchemaAST.getAnnotation(SensitiveId)(prop.type)) === true) { + if (Option.isSome(SchemaAST.resolveAt(SensitiveId)(prop.type)) === true) { fields.push(prop.name) } } diff --git a/packages/@overeng/restate-effect/src/schema/RestateError.ts b/packages/@overeng/restate-effect/src/schema/RestateError.ts index a1de15a534..16bef97f9a 100644 --- a/packages/@overeng/restate-effect/src/schema/RestateError.ts +++ b/packages/@overeng/restate-effect/src/schema/RestateError.ts @@ -7,13 +7,13 @@ import { formatReasonMessage } from '@overeng/utils' * * This is the error channel for the *wrapper's* own bridge operations * (durable `ctx.run`/`ctx.sleep`, serde, endpoint lifecycle, registration, - * ingress) — NOT the user's domain errors. Domain `Schema.TaggedError`s flow + * ingress) — NOT the user's domain errors. Domain `Schema.TaggedErrorClass`s flow * through the handler's own `E` channel and are mapped to a Restate * `TerminalError` at the endpoint boundary (see `Endpoint.toTerminal`). * * Mirrors `@overeng/pty-effect`'s `PtyError`: * - `reason` discriminator for programmatic matching - * - `cause: Schema.Defect` for the upstream error (preserves name + message) + * - `cause: Schema.Defect()` for the upstream error (preserves name + message) * - custom `get message()` for clean human-readable output in logs/traces * * Reasons: @@ -25,10 +25,10 @@ import { formatReasonMessage } from '@overeng/utils' * - `IngressFailed` — an external ingress client call failed * - `AdminFailed` — an admin / management API call failed (`./admin`) */ -export class RestateError extends Schema.TaggedError( +export class RestateError extends Schema.TaggedErrorClass( '@overeng/restate-effect/RestateError', )('RestateError', { - reason: Schema.Literal( + reason: Schema.Literals([ 'RunFailed', 'SleepFailed', 'SerdeFailed', @@ -36,9 +36,9 @@ export class RestateError extends Schema.TaggedError( 'RegistrationFailed', 'IngressFailed', 'AdminFailed', - ), + ]), method: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }) { override get message(): string { return formatReasonMessage({ reason: this.reason, method: this.method, cause: this.cause }) diff --git a/packages/@overeng/restate-effect/src/schema/Serde.ts b/packages/@overeng/restate-effect/src/schema/Serde.ts index 6f763c46f9..0ef7d8517a 100644 --- a/packages/@overeng/restate-effect/src/schema/Serde.ts +++ b/packages/@overeng/restate-effect/src/schema/Serde.ts @@ -1,5 +1,5 @@ import * as restate from '@restatedev/restate-sdk' -import { JSONSchema, Option, ParseResult, Schema } from 'effect' +import { JsonSchema, Option, ParseResult, Schema } from 'effect' import { textEncodeToArrayBuffer } from '@overeng/utils' @@ -35,7 +35,7 @@ const decoder = new TextDecoder() * transforms break the contract and are unsupported (docs/vrs/02-schema-serde/spec.md §1). * * `contentType` / `jsonSchema` default to `application/json` / - * `JSONSchema.make`, overridable via the `Restate.serde` annotation + * `JsonSchema.make`, overridable via the `Restate.serde` annotation * ([.decisions/0011](../../docs/vrs/.decisions/0011-restate-schema-annotations.md)). * * The `slot` controls decode-failure classification: an `ingress` decode @@ -75,7 +75,7 @@ export const effectSerde = ({ Option.getOrElse(() => (isVoid === true ? undefined : 'application/json')), ) const jsonSchema = Option.flatMap(overrides, (o) => Option.fromNullable(o.jsonSchema)).pipe( - Option.getOrElse(() => JSONSchema.make(schema) as object), + Option.getOrElse(() => JsonSchema.make(schema) as object), ) return { /* Omit `contentType` entirely when unset (void payload), per `exactOptionalPropertyTypes`. */ diff --git a/packages/@overeng/restate-effect/src/schema/redaction.integration.test.ts b/packages/@overeng/restate-effect/src/schema/redaction.integration.test.ts index 844ce46bac..9eb903b689 100644 --- a/packages/@overeng/restate-effect/src/schema/redaction.integration.test.ts +++ b/packages/@overeng/restate-effect/src/schema/redaction.integration.test.ts @@ -120,7 +120,7 @@ describe.skipIf(!serverAvailable)('sensitive-field redaction on the wire (real s rawIngressBody(harness.ingressUrl, 'redact-vault', 'reveal'), ) const wire = yield* Schema.decode( - Schema.parseJson(Schema.Struct({ label: Schema.String, token: Schema.String })), + Schema.fromJsonString(Schema.Struct({ label: Schema.String, token: Schema.String })), )(body) /* Non-sensitive field plaintext; sensitive field is NOT the plaintext. */ @@ -134,7 +134,9 @@ describe.skipIf(!serverAvailable)('sensitive-field redaction on the wire (real s const decrypted = new TextDecoder().decode( cipher.decrypt(Buffer.from(wire.token, 'base64')), ) - expect(yield* Schema.decode(Schema.parseJson(Schema.String))(decrypted)).toBe(PLAINTEXT) + expect(yield* Schema.decode(Schema.fromJsonString(Schema.String))(decrypted)).toBe( + PLAINTEXT, + ) }), ) }) diff --git a/packages/@overeng/restate-effect/src/testing/TestEnv.ts b/packages/@overeng/restate-effect/src/testing/TestEnv.ts index 5088ccf97a..eb1e1a56a9 100644 --- a/packages/@overeng/restate-effect/src/testing/TestEnv.ts +++ b/packages/@overeng/restate-effect/src/testing/TestEnv.ts @@ -158,10 +158,9 @@ export interface RestateTestEnvService { } /** The env service tag. The `mock` / `real` statics below are the only constructors. */ -export class RestateTestEnv extends Context.Tag('@overeng/restate-effect/RestateTestEnv')< - RestateTestEnv, - RestateTestEnvService ->() { +export class RestateTestEnv extends Context.Service()( + '@overeng/restate-effect/RestateTestEnv', +) { /** * The in-process MOCK backend: no journal, no server. Captures the * `Runtime` from `appLayer` once, then dispatches each contract-addressed @@ -266,7 +265,7 @@ const classifyMockExit = ({ outcome.errorTag !== undefined && errorSchema !== undefined ) { - const failure = Cause.failureOption(exit.cause) + const failure = Cause.findErrorOption(exit.cause) if (Option.isSome(failure) === true) { return Schema.encodeUnknown(errorSchema)(failure.value).pipe( Effect.flatMap((encoded) => Schema.decodeUnknown(errorSchema)(encoded)), diff --git a/packages/@overeng/restate-effect/src/testing/harness.integration.test.ts b/packages/@overeng/restate-effect/src/testing/harness.integration.test.ts index dd875358a1..04c5f52c9c 100644 --- a/packages/@overeng/restate-effect/src/testing/harness.integration.test.ts +++ b/packages/@overeng/restate-effect/src/testing/harness.integration.test.ts @@ -25,7 +25,7 @@ import { RestateTestHarness, serverAvailable } from './testing.ts' /* A trivial application service the handler depends on, to prove `appLayer` is * threaded into the served runtime (handler `R` is satisfied). */ -class Step extends Context.Tag('test/Step')() { +class Step extends Context.Service()('test/Step') { static readonly Default = Layer.succeed(Step, { by: 1 }) } diff --git a/packages/@overeng/restate-effect/src/testing/test-env.integration.test.ts b/packages/@overeng/restate-effect/src/testing/test-env.integration.test.ts index 0440dea651..2dcfa16093 100644 --- a/packages/@overeng/restate-effect/src/testing/test-env.integration.test.ts +++ b/packages/@overeng/restate-effect/src/testing/test-env.integration.test.ts @@ -24,11 +24,13 @@ import { RestateTestEnv, serverAvailable } from './testing.ts' /* ── demo app: a greeter Service (typed error) + a counter Object (typed State) ── */ -class Greeting extends Context.Tag('test-env/Greeting')() { +class Greeting extends Context.Service()( + 'test-env/Greeting', +) { static readonly Default = Layer.succeed(Greeting, { prefix: 'Hello' }) } -class EmptyName extends Schema.TaggedError('test-env/EmptyName')('EmptyName', {}) {} +class EmptyName extends Schema.TaggedErrorClass('test-env/EmptyName')('EmptyName', {}) {} const Greeter = RestateService.contract({ name: 'test-env-greeter', @@ -94,7 +96,7 @@ const Cursor = State.for(CursorState) /* `peek` returns the watermark inside a STRUCT with an optional property (a valid * JSON schema), since a top-level `Schema.UndefinedOr` handler return breaks - * `JSONSchema.make` at endpoint registration — State, not handler I/O, is the + * `JsonSchema.make` at endpoint registration — State, not handler I/O, is the * nullable surface (#1). */ const PeekOutput = Schema.Struct({ highWatermark: Schema.optional(Schema.Number) }) diff --git a/packages/@overeng/restate-effect/src/testing/testing.ts b/packages/@overeng/restate-effect/src/testing/testing.ts index b31acd1214..0b8b69c3c2 100644 --- a/packages/@overeng/restate-effect/src/testing/testing.ts +++ b/packages/@overeng/restate-effect/src/testing/testing.ts @@ -748,10 +748,10 @@ export interface BoundIngress { /* eslint-enable @typescript-eslint/no-explicit-any */ /** The harness service tag. The `layer` factory below is the only constructor. */ -export class RestateTestHarness extends Context.Tag('@overeng/restate-effect/RestateTestHarness')< +export class RestateTestHarness extends Context.Service< RestateTestHarness, RestateTestHarnessService ->() { +>()('@overeng/restate-effect/RestateTestHarness') { /** * A scoped `Layer` that, on ACQUIRE: allocates an isolated temp base dir + * EPHEMERAL ports for every listener (server ingress / admin / node-to-node AND diff --git a/packages/@overeng/tui-react/docs/spec.md b/packages/@overeng/tui-react/docs/spec.md index 31b78966c7..aacc49c56d 100644 --- a/packages/@overeng/tui-react/docs/spec.md +++ b/packages/@overeng/tui-react/docs/spec.md @@ -683,7 +683,7 @@ function MyView({ state }) { Commands use `createTuiApp` with Elm architecture (State + Action + Reducer) and atom-based views: ```typescript -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import * as Cli from '@effect/cli' import { createTuiApp, useTuiAtomValue, outputOption, outputModeLayer, Box, Text, Spinner } from '@overeng/tui-react' @@ -908,12 +908,12 @@ test('deploy dispatches correct actions', async () => { ### Testing React Components ```typescript -import { Atom, Registry } from '@effect-atom/atom' +import { Atom, AtomRegistry } from 'effect/unstable/reactivity' import { TestRenderer, TuiRegistryContext } from '@overeng/tui-react/test' test('deploy view renders progress', () => { const renderer = TestRenderer.create() - const registry = Registry.make() + const registry = AtomRegistry.make() const stateAtom = Atom.make({ _tag: 'Progress', services: [{ name: 'api-server', status: 'healthy' }], @@ -1010,7 +1010,7 @@ const MyView = ({ stateAtom }: { stateAtom: Atom.Atom }) => { Views receive a `stateAtom` prop and subscribe internally using `useTuiAtomValue`: ```typescript -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import { useTuiAtomValue } from '@overeng/tui-react' interface MyViewProps { @@ -1031,7 +1031,7 @@ const tui = yield* MyApp.run() // Usage in tests: -const registry = Registry.make() +const registry = AtomRegistry.make() const stateAtom = Atom.make(testState) @@ -1161,7 +1161,7 @@ interface MyViewProps { A full deploy command using Effect CLI with tui-react and atom-based views: ```typescript -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import * as Cli from '@effect/cli' import { NodeContext, NodeRuntime } from '@effect/platform-node' import { Effect, Schema, Duration } from 'effect' @@ -1331,7 +1331,7 @@ Cli.Command.run(deployCommand, { name: 'deploy', version: '1.0.0' })(process.arg | Decision | Choice | Rationale | | ------------------ | --------------------------- | -------------------------------------- | | CLI integration | `@effect/cli` | Leverage existing framework | -| State primitive | `@effect-atom/atom` | Better React integration, sync updates | +| State primitive | `effect/unstable/reactivity` | Better React integration, sync updates | | State updates | Reducer-only (Elm) | Predictable, testable | | Mode selection | `OutputMode` service | Layer-based, composable | | Inline reconciler | Custom (`react-reconciler`) | Full control, no ink dependency | diff --git a/packages/@overeng/tui-react/examples/01-basic/hello-world.tsx b/packages/@overeng/tui-react/examples/01-basic/hello-world.tsx index 402016cd71..4b3d9aaa7c 100644 --- a/packages/@overeng/tui-react/examples/01-basic/hello-world.tsx +++ b/packages/@overeng/tui-react/examples/01-basic/hello-world.tsx @@ -14,9 +14,9 @@ * bun examples/01-basic/hello-world.tsx --help */ -import { Command, Options } from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect } from 'effect' +import { Command, Flag as Options } from 'effect/unstable/cli' import React from 'react' import { createTuiApp, run } from '../../src/mod.tsx' @@ -83,10 +83,9 @@ const helloWorldCommand = Command.make( ({ duration, output }) => runHelloWorld(duration).pipe(Effect.provide(outputModeLayer(output))), ) -const cli = Command.run(helloWorldCommand, { - name: 'Hello World', +const cli = Command.runWith(helloWorldCommand, { version: '1.0.0', }) // Run with Effect CLI (handles SIGINT/SIGTERM properly) -cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +cli(process.argv.slice(2)).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain) diff --git a/packages/@overeng/tui-react/examples/01-basic/schema.ts b/packages/@overeng/tui-react/examples/01-basic/schema.ts index 5e03119b28..88b1a8d591 100644 --- a/packages/@overeng/tui-react/examples/01-basic/schema.ts +++ b/packages/@overeng/tui-react/examples/01-basic/schema.ts @@ -22,7 +22,7 @@ export const FinishedState = Schema.TaggedStruct('Finished', { export const InterruptedState = Schema.TaggedStruct('Interrupted', {}) /** Union schema of all hello world app states. */ -export const AppState = Schema.Union(DisplayingState, FinishedState, InterruptedState) +export const AppState = Schema.Union([DisplayingState, FinishedState, InterruptedState]) /** Inferred type for the hello world app state union. */ export type AppState = Schema.Schema.Type @@ -32,11 +32,11 @@ export type AppState = Schema.Schema.Type // ============================================================================= /** Union schema of all hello world app actions (Tick, Finish, Interrupted). */ -export const AppAction = Schema.Union( +export const AppAction = Schema.Union([ Schema.TaggedStruct('Tick', {}), Schema.TaggedStruct('Finish', {}), Schema.TaggedStruct('Interrupted', {}), -) +]) /** Inferred type for the hello world app action union. */ export type AppAction = Schema.Schema.Type diff --git a/packages/@overeng/tui-react/examples/01-basic/view.tsx b/packages/@overeng/tui-react/examples/01-basic/view.tsx index 4c22354993..b20b747590 100644 --- a/packages/@overeng/tui-react/examples/01-basic/view.tsx +++ b/packages/@overeng/tui-react/examples/01-basic/view.tsx @@ -1,4 +1,4 @@ -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import React, { useMemo } from 'react' import { Box, Text, useTuiAtomValue } from '../../src/mod.tsx' diff --git a/packages/@overeng/tui-react/examples/02-effect-integration/counter.tsx b/packages/@overeng/tui-react/examples/02-effect-integration/counter.tsx index 07a25fc6b9..21f867789c 100644 --- a/packages/@overeng/tui-react/examples/02-effect-integration/counter.tsx +++ b/packages/@overeng/tui-react/examples/02-effect-integration/counter.tsx @@ -16,9 +16,9 @@ * bun examples/03-effect-integration/counter.tsx --help */ -import { Command } from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Duration, Effect } from 'effect' +import { Command } from 'effect/unstable/cli' import React from 'react' import { createTuiApp, run } from '../../src/mod.tsx' @@ -85,10 +85,9 @@ const counter = Command.make('counter', { output: outputOption }, ({ output }) = runCounter.pipe(Effect.provide(outputModeLayer(output))), ) -const cli = Command.run(counter, { - name: 'counter', +const cli = Command.runWith(counter, { version: '1.0.0', }) // Run with Effect CLI (handles SIGINT/SIGTERM properly) -cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +cli(process.argv.slice(2)).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain) diff --git a/packages/@overeng/tui-react/examples/02-effect-integration/schema.ts b/packages/@overeng/tui-react/examples/02-effect-integration/schema.ts index 2328f3ec93..ef688c685d 100644 --- a/packages/@overeng/tui-react/examples/02-effect-integration/schema.ts +++ b/packages/@overeng/tui-react/examples/02-effect-integration/schema.ts @@ -11,7 +11,7 @@ import { Schema } from 'effect' /** Schema for the running counter state with count, loading status, and action history. */ export const RunningState = Schema.TaggedStruct('Running', { count: Schema.Number, - status: Schema.Literal('idle', 'loading'), + status: Schema.Literals(['idle', 'loading']), history: Schema.Array(Schema.String), }) @@ -28,7 +28,7 @@ export const InterruptedState = Schema.TaggedStruct('Interrupted', { }) /** Union schema of all counter states (Running, Complete, Interrupted). */ -export const CounterState = Schema.Union(RunningState, CompleteState, InterruptedState) +export const CounterState = Schema.Union([RunningState, CompleteState, InterruptedState]) export type CounterState = typeof CounterState.Type @@ -37,13 +37,13 @@ export type CounterState = typeof CounterState.Type // ============================================================================= /** Union schema of counter actions (Increment, Decrement, SetLoading, SetComplete, Interrupted). */ -export const CounterAction = Schema.Union( +export const CounterAction = Schema.Union([ Schema.TaggedStruct('Increment', {}), Schema.TaggedStruct('Decrement', {}), Schema.TaggedStruct('SetLoading', {}), Schema.TaggedStruct('SetComplete', { message: Schema.String }), Schema.TaggedStruct('Interrupted', {}), -) +]) export type CounterAction = typeof CounterAction.Type diff --git a/packages/@overeng/tui-react/examples/02-effect-integration/view.tsx b/packages/@overeng/tui-react/examples/02-effect-integration/view.tsx index 4fd78ce5eb..956b8736ec 100644 --- a/packages/@overeng/tui-react/examples/02-effect-integration/view.tsx +++ b/packages/@overeng/tui-react/examples/02-effect-integration/view.tsx @@ -1,4 +1,4 @@ -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import React, { useMemo } from 'react' import { Box, Text, Spinner, useTuiAtomValue } from '../../src/mod.tsx' diff --git a/packages/@overeng/tui-react/examples/03-cli/deploy/deploy.tsx b/packages/@overeng/tui-react/examples/03-cli/deploy/deploy.tsx index a0edb53609..2717c8e217 100644 --- a/packages/@overeng/tui-react/examples/03-cli/deploy/deploy.tsx +++ b/packages/@overeng/tui-react/examples/03-cli/deploy/deploy.tsx @@ -5,7 +5,7 @@ */ import { Schema } from 'effect' -import { Duration, Effect } from 'effect' +import { Duration, Effect, Result } from 'effect' import React from 'react' import type { OutputModeTag, TuiAppApi } from '../../../src/mod.tsx' @@ -25,7 +25,7 @@ import { import { DeployView } from './view.tsx' /** Tagged error type for deployment failures. */ -export class DeployError extends Schema.TaggedError()('DeployError', { +export class DeployError extends Schema.TaggedErrorClass()('DeployError', { message: Schema.String, }) {} @@ -230,17 +230,17 @@ export const runDeploy = ( }, }) - const pullResult = yield* simulatePull(service).pipe(Effect.either) - if (pullResult._tag === 'Left') { + const pullResult = yield* simulatePull(service).pipe(Effect.result) + if (Result.isFailure(pullResult) === true) { failed = true failedService = service - failedError = pullResult.left.message - log({ level: 'error', message: pullResult.left.message, service }) + failedError = pullResult.failure.message + log({ level: 'error', message: pullResult.failure.message, service }) results.push({ name: service, result: 'failed', duration: Date.now() - serviceStart, - error: pullResult.left.message, + error: pullResult.failure.message, }) break } @@ -256,17 +256,17 @@ export const runDeploy = ( }, }) - const startResult = yield* simulateStart(service).pipe(Effect.either) - if (startResult._tag === 'Left') { + const startResult = yield* simulateStart(service).pipe(Effect.result) + if (Result.isFailure(startResult) === true) { failed = true failedService = service - failedError = startResult.left.message - log({ level: 'error', message: startResult.left.message, service }) + failedError = startResult.failure.message + log({ level: 'error', message: startResult.failure.message, service }) results.push({ name: service, result: 'failed', duration: Date.now() - serviceStart, - error: startResult.left.message, + error: startResult.failure.message, }) break } @@ -282,17 +282,17 @@ export const runDeploy = ( }, }) - const healthResult = yield* simulateHealthcheck(service).pipe(Effect.either) - if (healthResult._tag === 'Left') { + const healthResult = yield* simulateHealthcheck(service).pipe(Effect.result) + if (Result.isFailure(healthResult) === true) { failed = true failedService = service - failedError = healthResult.left.message - log({ level: 'error', message: healthResult.left.message, service }) + failedError = healthResult.failure.message + log({ level: 'error', message: healthResult.failure.message, service }) results.push({ name: service, result: 'failed', duration: Date.now() - serviceStart, - error: healthResult.left.message, + error: healthResult.failure.message, }) break } diff --git a/packages/@overeng/tui-react/examples/03-cli/deploy/main.ts b/packages/@overeng/tui-react/examples/03-cli/deploy/main.ts index 0254745153..418bbcc1e4 100644 --- a/packages/@overeng/tui-react/examples/03-cli/deploy/main.ts +++ b/packages/@overeng/tui-react/examples/03-cli/deploy/main.ts @@ -16,9 +16,9 @@ * bun examples/03-cli/deploy/main.ts --help */ -import { Command, Options } from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect } from 'effect' +import { Command, Flag as Options } from 'effect/unstable/cli' import { outputOption, outputModeLayer } from '../../../src/node/mod.ts' import { DeployError, runDeploy } from './deploy.tsx' @@ -27,12 +27,12 @@ import { DeployError, runDeploy } from './deploy.tsx' // Command Options // ============================================================================= -const services = Options.text('services').pipe( +const services = Options.string('services').pipe( Options.withAlias('s'), Options.withDescription('Comma-separated list of services to deploy'), ) -const env = Options.text('env').pipe( +const env = Options.string('env').pipe( Options.withAlias('e'), Options.withDefault('production'), Options.withDescription('Environment to deploy to'), @@ -103,10 +103,9 @@ const deploy = Command.make( // CLI Runner // ============================================================================= -const cli = Command.run(deploy, { - name: 'deploy', +const cli = Command.runWith(deploy, { version: '1.0.0', }) // Run with Effect CLI (handles SIGINT/SIGTERM properly) -cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +cli(process.argv.slice(2)).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain) diff --git a/packages/@overeng/tui-react/examples/03-cli/deploy/schema.ts b/packages/@overeng/tui-react/examples/03-cli/deploy/schema.ts index 6fae6cbc5a..c5ede18b42 100644 --- a/packages/@overeng/tui-react/examples/03-cli/deploy/schema.ts +++ b/packages/@overeng/tui-react/examples/03-cli/deploy/schema.ts @@ -5,21 +5,21 @@ * Used for both visual rendering and JSON serialization. */ -import { Schema } from 'effect' +import { Effect, Schema } from 'effect' // ============================================================================= // Service Status // ============================================================================= /** Schema for the lifecycle status of a service during deployment. */ -export const ServiceStatus = Schema.Literal( +export const ServiceStatus = Schema.Literals([ 'pending', 'pulling', 'starting', 'healthcheck', 'healthy', 'failed', -) +]) /** Inferred type for a service's deployment lifecycle status. */ export type ServiceStatus = Schema.Schema.Type @@ -39,7 +39,7 @@ export type ServiceProgress = Schema.Schema.Type /** Schema for a service's final deployment result (updated, unchanged, rolled-back, or failed). */ export const ServiceResult = Schema.Struct({ name: Schema.String, - result: Schema.Literal('updated', 'unchanged', 'rolled-back', 'failed'), + result: Schema.Literals(['updated', 'unchanged', 'rolled-back', 'failed']), duration: Schema.Number, error: Schema.optional(Schema.String), }) @@ -53,7 +53,7 @@ export type ServiceResult = Schema.Schema.Type /** Schema for a timestamped log entry with level, message, and optional service context. */ export const LogEntry = Schema.Struct({ timestamp: Schema.String, - level: Schema.Literal('info', 'warn', 'error', 'debug'), + level: Schema.Literals(['info', 'warn', 'error', 'debug']), message: Schema.String, service: Schema.optional(Schema.String), }) @@ -65,7 +65,7 @@ export type LogEntry = Schema.Schema.Type // ============================================================================= /** Union schema of all deploy states (Idle, Validating, Progress, Complete, Failed, RollingBack, Interrupted). */ -export const DeployState = Schema.Union( +export const DeployState = Schema.Union([ Schema.TaggedStruct('Idle', {}), Schema.TaggedStruct('Validating', { @@ -114,7 +114,7 @@ export const DeployState = Schema.Union( startedAt: Schema.Number, interruptedAt: Schema.Number, }), -) +]) /** Inferred type for the deploy state union. */ export type DeployState = Schema.Schema.Type @@ -127,9 +127,18 @@ export type DeployState = Schema.Schema.Type export const DeployOptions = Schema.Struct({ services: Schema.Array(Schema.String), environment: Schema.String, - dryRun: Schema.optionalWith(Schema.Boolean, { default: () => false }), - force: Schema.optionalWith(Schema.Boolean, { default: () => false }), - timeout: Schema.optionalWith(Schema.Number, { default: () => 30000 }), + dryRun: Schema.Boolean.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => false)), + Schema.withConstructorDefault(Effect.sync(() => false)), + ), + force: Schema.Boolean.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => false)), + Schema.withConstructorDefault(Effect.sync(() => false)), + ), + timeout: Schema.Number.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => 30000)), + Schema.withConstructorDefault(Effect.sync(() => 30000)), + ), }) /** Inferred type for deploy command options. */ export type DeployOptions = Schema.Schema.Type @@ -153,16 +162,16 @@ export type DeployResult = Schema.Schema.Type // ============================================================================= /** Events emitted in NDJSON mode instead of full state snapshots */ -export const DeployNdjsonEvent = Schema.Union( +export const DeployNdjsonEvent = Schema.Union([ Schema.TaggedStruct('PhaseChanged', { - phase: Schema.Literal( + phase: Schema.Literals([ 'Validating', 'Progress', 'Complete', 'Failed', 'RollingBack', 'Interrupted', - ), + ]), }), Schema.TaggedStruct('ServiceStatusChanged', { name: Schema.String, @@ -170,7 +179,7 @@ export const DeployNdjsonEvent = Schema.Union( message: Schema.optional(Schema.String), }), Schema.TaggedStruct('LogAdded', { log: LogEntry }), -) +]) export type DeployNdjsonEvent = typeof DeployNdjsonEvent.Type type Phase = DeployNdjsonEvent extends { _tag: 'PhaseChanged'; phase: infer P } ? P : never @@ -244,7 +253,7 @@ export const deployFromAction = ({ // ============================================================================= /** Union schema of deploy actions (SetState, UpdateServiceStatus, AddLog, Interrupted). */ -export const DeployAction = Schema.Union( +export const DeployAction = Schema.Union([ // Direct state transitions Schema.TaggedStruct('SetState', { state: DeployState }), @@ -260,7 +269,7 @@ export const DeployAction = Schema.Union( // Interrupt handling (auto-dispatched on Ctrl+C) Schema.TaggedStruct('Interrupted', {}), -) +]) /** Inferred type for the deploy action union. */ export type DeployAction = Schema.Schema.Type diff --git a/packages/@overeng/tui-react/examples/03-cli/deploy/view.tsx b/packages/@overeng/tui-react/examples/03-cli/deploy/view.tsx index 4dc034631d..61850965fe 100644 --- a/packages/@overeng/tui-react/examples/03-cli/deploy/view.tsx +++ b/packages/@overeng/tui-react/examples/03-cli/deploy/view.tsx @@ -2,7 +2,7 @@ * Deploy CLI View Component */ -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import React, { useMemo } from 'react' import { Box, Text, Spinner, Static, useTuiAtomValue } from '../../../src/mod.tsx' diff --git a/packages/@overeng/tui-react/examples/04-stress-tests/rapid-updates.tsx b/packages/@overeng/tui-react/examples/04-stress-tests/rapid-updates.tsx index 5acc2ccd9f..f318fa5a9e 100644 --- a/packages/@overeng/tui-react/examples/04-stress-tests/rapid-updates.tsx +++ b/packages/@overeng/tui-react/examples/04-stress-tests/rapid-updates.tsx @@ -14,9 +14,9 @@ * Run with JSON: bun examples/05-stress-tests/rapid-updates.tsx --json */ -import { Command, Options } from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect, Fiber } from 'effect' +import { Command, Flag as Options } from 'effect/unstable/cli' import React from 'react' import { createTuiApp, run } from '../../src/mod.tsx' @@ -64,7 +64,7 @@ const runStressTest = (durationMs: number) => { (tui) => Effect.gen(function* () { // Run the animation loop - const animationFiber = yield* Effect.fork( + const animationFiber = yield* Effect.forkChild( Effect.gen(function* () { while (tui.getState()._tag === 'Running') { tui.dispatch({ _tag: 'Tick' }) @@ -102,10 +102,9 @@ const stressTestCommand = Command.make( runStressTest(duration * 1000).pipe(Effect.provide(outputModeLayer(output))), ) -const cli = Command.run(stressTestCommand, { - name: 'Rapid Updates Stress Test', +const cli = Command.runWith(stressTestCommand, { version: '1.0.0', }) // Run with Effect CLI (handles SIGINT/SIGTERM properly) -cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +cli(process.argv.slice(2)).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain) diff --git a/packages/@overeng/tui-react/examples/04-stress-tests/schema.ts b/packages/@overeng/tui-react/examples/04-stress-tests/schema.ts index 4ec1b27e26..bdda5d7c55 100644 --- a/packages/@overeng/tui-react/examples/04-stress-tests/schema.ts +++ b/packages/@overeng/tui-react/examples/04-stress-tests/schema.ts @@ -31,7 +31,7 @@ export const InterruptedState = Schema.TaggedStruct('Interrupted', { }) /** Union schema of all stress test states. */ -export const StressTestState = Schema.Union(RunningState, FinishedState, InterruptedState) +export const StressTestState = Schema.Union([RunningState, FinishedState, InterruptedState]) /** Inferred type for the stress test state union. */ export type StressTestState = Schema.Schema.Type @@ -41,11 +41,11 @@ export type StressTestState = Schema.Schema.Type // ============================================================================= /** Union schema of stress test actions (Tick, Finish, Interrupted). */ -export const StressTestAction = Schema.Union( +export const StressTestAction = Schema.Union([ Schema.TaggedStruct('Tick', {}), Schema.TaggedStruct('Finish', {}), Schema.TaggedStruct('Interrupted', {}), -) +]) /** Inferred type for the stress test action union. */ export type StressTestAction = Schema.Schema.Type diff --git a/packages/@overeng/tui-react/examples/04-stress-tests/view.tsx b/packages/@overeng/tui-react/examples/04-stress-tests/view.tsx index 4ef3c6f964..084882c405 100644 --- a/packages/@overeng/tui-react/examples/04-stress-tests/view.tsx +++ b/packages/@overeng/tui-react/examples/04-stress-tests/view.tsx @@ -1,4 +1,4 @@ -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import React, { useMemo } from 'react' import { Box, Text, useTuiAtomValue } from '../../src/mod.tsx' diff --git a/packages/@overeng/tui-react/examples/05-advanced/bouncing-windows.tsx b/packages/@overeng/tui-react/examples/05-advanced/bouncing-windows.tsx index aa83655a60..c4dd023af4 100644 --- a/packages/@overeng/tui-react/examples/05-advanced/bouncing-windows.tsx +++ b/packages/@overeng/tui-react/examples/05-advanced/bouncing-windows.tsx @@ -17,9 +17,9 @@ * bun examples/06-advanced/bouncing-windows.tsx --help */ -import { Command, Options } from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect, Fiber } from 'effect' +import { Command, Flag as Options } from 'effect/unstable/cli' import React from 'react' import { createTuiApp, run } from '../../src/mod.tsx' @@ -96,7 +96,7 @@ const runBouncingWindows = ({ process.stdout.on('resize', resizeHandler) // Animation loop - const animationFiber = yield* Effect.fork( + const animationFiber = yield* Effect.forkChild( Effect.gen(function* () { while (tui.getState()._tag === 'Running') { tui.dispatch({ _tag: 'Tick' }) @@ -138,10 +138,9 @@ const bouncingWindowsCommand = Command.make( ), ) -const cli = Command.run(bouncingWindowsCommand, { - name: 'Bouncing Windows Demo', +const cli = Command.runWith(bouncingWindowsCommand, { version: '1.0.0', }) // Run with Effect CLI (handles SIGINT/SIGTERM properly) -cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +cli(process.argv.slice(2)).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain) diff --git a/packages/@overeng/tui-react/examples/05-advanced/schema.ts b/packages/@overeng/tui-react/examples/05-advanced/schema.ts index 8ddef48fb0..7a9da20f92 100644 --- a/packages/@overeng/tui-react/examples/05-advanced/schema.ts +++ b/packages/@overeng/tui-react/examples/05-advanced/schema.ts @@ -142,7 +142,7 @@ const WindowSchema = Schema.Struct({ width: Schema.Number, height: Schema.Number, title: Schema.String, - color: Schema.Literal('red', 'green', 'yellow', 'blue', 'magenta', 'cyan'), + color: Schema.Literals(['red', 'green', 'yellow', 'blue', 'magenta', 'cyan']), stats: Schema.Struct({ cpu: Schema.Number, mem: Schema.Number, @@ -172,7 +172,7 @@ export const InterruptedState = Schema.TaggedStruct('Interrupted', { }) /** Union schema of all bouncing windows app states. */ -export const AppState = Schema.Union(RunningState, FinishedState, InterruptedState) +export const AppState = Schema.Union([RunningState, FinishedState, InterruptedState]) /** Inferred type for the bouncing windows app state union. */ export type AppState = Schema.Schema.Type @@ -182,12 +182,12 @@ export type AppState = Schema.Schema.Type // ============================================================================= /** Union schema of bouncing windows actions (Tick, Resize, Finish, Interrupted). */ -export const AppAction = Schema.Union( +export const AppAction = Schema.Union([ Schema.TaggedStruct('Tick', {}), Schema.TaggedStruct('Resize', { width: Schema.Number, height: Schema.Number }), Schema.TaggedStruct('Finish', {}), Schema.TaggedStruct('Interrupted', {}), -) +]) /** Inferred type for the bouncing windows app action union. */ export type AppAction = Schema.Schema.Type diff --git a/packages/@overeng/tui-react/examples/05-advanced/view.tsx b/packages/@overeng/tui-react/examples/05-advanced/view.tsx index 5d775f6756..8d02246413 100644 --- a/packages/@overeng/tui-react/examples/05-advanced/view.tsx +++ b/packages/@overeng/tui-react/examples/05-advanced/view.tsx @@ -1,4 +1,4 @@ -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import React, { useMemo } from 'react' import { Box, Text, useTuiAtomValue } from '../../src/mod.tsx' diff --git a/packages/@overeng/tui-react/examples/06-log-capture/log-capture.tsx b/packages/@overeng/tui-react/examples/06-log-capture/log-capture.tsx index b8df171a7c..67ad3dc5ce 100644 --- a/packages/@overeng/tui-react/examples/06-log-capture/log-capture.tsx +++ b/packages/@overeng/tui-react/examples/06-log-capture/log-capture.tsx @@ -10,9 +10,9 @@ * bun examples/06-log-capture/log-capture.tsx --output json */ -import { Command } from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Duration, Effect } from 'effect' +import { Command } from 'effect/unstable/cli' import React from 'react' import { createTuiApp, run } from '../../src/mod.tsx' @@ -70,9 +70,8 @@ const logCaptureCmd = Command.make('log-capture', { output: outputOption }, ({ o runTaskRunner.pipe(Effect.provide(outputModeLayer(output))), ) -const cli = Command.run(logCaptureCmd, { - name: 'log-capture', +const cli = Command.runWith(logCaptureCmd, { version: '1.0.0', }) -cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +cli(process.argv.slice(2)).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain) diff --git a/packages/@overeng/tui-react/examples/06-log-capture/schema.ts b/packages/@overeng/tui-react/examples/06-log-capture/schema.ts index 92021e7c26..ac5c0dc95d 100644 --- a/packages/@overeng/tui-react/examples/06-log-capture/schema.ts +++ b/packages/@overeng/tui-react/examples/06-log-capture/schema.ts @@ -13,7 +13,7 @@ import { Schema } from 'effect' // ============================================================================= /** Schema for task status values. */ -export const TaskStatus = Schema.Literal('pending', 'running', 'done', 'error') +export const TaskStatus = Schema.Literals(['pending', 'running', 'done', 'error']) /** Schema for a single task item with name and status. */ export const TaskItem = Schema.Struct({ @@ -39,7 +39,7 @@ export const InterruptedState = Schema.TaggedStruct('Interrupted', { }) /** Union schema of all task runner states (Running, Complete, Interrupted). */ -export const TaskRunnerState = Schema.Union(RunningState, CompleteState, InterruptedState) +export const TaskRunnerState = Schema.Union([RunningState, CompleteState, InterruptedState]) export type TaskRunnerState = typeof TaskRunnerState.Type @@ -48,13 +48,13 @@ export type TaskRunnerState = typeof TaskRunnerState.Type // ============================================================================= /** Union schema of task runner actions (StartTask, CompleteTask, FailTask, Finish, Interrupted). */ -export const TaskRunnerAction = Schema.Union( +export const TaskRunnerAction = Schema.Union([ Schema.TaggedStruct('StartTask', { name: Schema.String }), Schema.TaggedStruct('CompleteTask', { name: Schema.String }), Schema.TaggedStruct('FailTask', { name: Schema.String }), Schema.TaggedStruct('Finish', {}), Schema.TaggedStruct('Interrupted', {}), -) +]) export type TaskRunnerAction = typeof TaskRunnerAction.Type diff --git a/packages/@overeng/tui-react/examples/06-log-capture/view.tsx b/packages/@overeng/tui-react/examples/06-log-capture/view.tsx index fe5ae08d46..ba409742cc 100644 --- a/packages/@overeng/tui-react/examples/06-log-capture/view.tsx +++ b/packages/@overeng/tui-react/examples/06-log-capture/view.tsx @@ -5,7 +5,7 @@ * output in the Static region, above dynamic task progress. */ -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import React, { useMemo } from 'react' import { Box, Text, Static, Spinner, useTuiAtomValue, useCapturedLogs } from '../../src/mod.tsx' diff --git a/packages/@overeng/tui-react/examples/viewport-overflow-stress.tsx b/packages/@overeng/tui-react/examples/viewport-overflow-stress.tsx index a3f12e4bdd..16ff16f4e3 100644 --- a/packages/@overeng/tui-react/examples/viewport-overflow-stress.tsx +++ b/packages/@overeng/tui-react/examples/viewport-overflow-stress.tsx @@ -15,9 +15,9 @@ * bun examples/viewport-overflow-stress.tsx footer 200 10 # 200 files, 10ms delay */ -import type { Atom } from '@effect-atom/atom' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect, Schema } from 'effect' +import type { Atom } from 'effect/unstable/reactivity' import React, { useMemo } from 'react' import { createTuiApp, run, Box, Text, Spinner, useViewport, useTuiAtomValue } from '../src/mod.tsx' @@ -34,7 +34,7 @@ const FileEntry = Schema.Struct({ status: Schema.String, }) -const AppState = Schema.Union( +const AppState = Schema.Union([ Schema.TaggedStruct('Discovering', { files: Schema.Array(FileEntry), }), @@ -48,11 +48,11 @@ const AppState = Schema.Union( total: Schema.Number, }), Schema.TaggedStruct('Interrupted', {}), -) +]) type AppState = typeof AppState.Type -const AppAction = Schema.Union( +const AppAction = Schema.Union([ Schema.TaggedStruct('StartGenerating', {}), Schema.TaggedStruct('ProcessFile', { index: Schema.Number, @@ -63,7 +63,7 @@ const AppAction = Schema.Union( }), Schema.TaggedStruct('Finish', {}), Schema.TaggedStruct('Interrupted', {}), -) +]) type AppAction = typeof AppAction.Type @@ -210,11 +210,11 @@ const StressView = ({ stateAtom }: { stateAtom: Atom.Atom }) => { // Static scenarios (one-shot, for comparison) // ============================================================================= -const VerticalState = Schema.Union( +const VerticalState = Schema.Union([ Schema.TaggedStruct('Showing', { count: Schema.Number }), Schema.TaggedStruct('Interrupted', {}), -) -const VerticalAction = Schema.Union(Schema.TaggedStruct('Interrupted', {})) +]) +const VerticalAction = Schema.Union([Schema.TaggedStruct('Interrupted', {})]) const verticalReducer = ({ state, action, @@ -357,4 +357,4 @@ const program = Effect.gen(function* () { } }).pipe(Effect.provide(outputModeLayer('tty'))) -program.pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain) +program.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain) diff --git a/packages/@overeng/tui-react/package.json b/packages/@overeng/tui-react/package.json index 86f24880f9..749022bd17 100644 --- a/packages/@overeng/tui-react/package.json +++ b/packages/@overeng/tui-react/package.json @@ -25,14 +25,9 @@ "test:e2e:ui": "playwright test --ui" }, "dependencies": { - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@overeng/tui-core": "workspace:^", "@overeng/utils": "workspace:^", "@playwright/test": "1.61.0", @@ -41,18 +36,15 @@ "@xterm/headless": "6.0.0", "@xterm/xterm": "6.0.0", "cli-truncate": "6.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "string-width": "8.2.1", "vitest": "4.1.9", "yoga-layout": "3.2.1" }, "devDependencies": { - "@effect-atom/atom": "0.5.3", - "@effect-atom/atom-react": "0.5.0", - "@effect/cli": "0.75.2", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/vitest": "0.29.0", + "@effect/atom-react": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@opentui/core": "0.4.1", "@opentui/react": "0.4.1", "@overeng/utils-dev": "workspace:^", @@ -63,7 +55,7 @@ "@types/react": "19.2.17", "@types/react-reconciler": "0.33.0", "@vitejs/plugin-react": "6.0.2", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-dom": "19.2.7", "react-reconciler": "0.33.0", @@ -73,16 +65,14 @@ "vitest": "4.1.9" }, "peerDependencies": { - "@effect-atom/atom": "^0.5.3", - "@effect-atom/atom-react": "^0.5.0", - "@effect/cli": "^0.75.2", - "@effect/platform-node": "^0.107.0", + "@effect/atom-react": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@opentui/core": "^0.4.1", "@opentui/react": "^0.4.1", "@storybook/react": "^10.4.6", "@types/react": "^19.2.17", "@types/react-reconciler": "^0.33.0", - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "react": "^19.2.7", "react-dom": "^19.2.7", "react-reconciler": "^0.33.0" diff --git a/packages/@overeng/tui-react/package.json.genie.ts b/packages/@overeng/tui-react/package.json.genie.ts index 4d4b120fa6..17e42ea67a 100644 --- a/packages/@overeng/tui-react/package.json.genie.ts +++ b/packages/@overeng/tui-react/package.json.genie.ts @@ -12,58 +12,22 @@ import utilsDevPkg from '../utils-dev/package.json.genie.ts' import utilsPkg from '../utils/package.json.genie.ts' /** Runtime + type peer deps — consumers must have these to use and type-check tui-react's .tsx source exports */ -const peerDepNames = [ - 'effect', - 'react', - 'react-dom', - 'react-reconciler', - '@effect/platform-node', - '@effect/cli', - /** Required for consumers to type-check imported .tsx source (not compiled .d.ts) */ - '@types/react', - '@types/react-reconciler', - /** The ./storybook export is consumed by other packages that already have storybook */ - '@storybook/react', -] as const -const effectAtomDeps = ['@effect-atom/atom', '@effect-atom/atom-react'] as const +const peerDepNames = ['effect', 'react', 'react-dom', 'react-reconciler', '@effect/platform-node', /** Required for consumers to type-check imported .tsx source (not compiled .d.ts) */ + '@types/react', '@types/react-reconciler', /** The ./storybook export is consumed by other packages that already have storybook */ + '@storybook/react'] as const +const effectAtomDeps = ['@effect/atom-react'] as const const opentuiDeps = ['@opentui/core', '@opentui/react'] as const const runtimeDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/tui-react' }), dependencies: { workspace: [tuiCorePkg, utilsPkg], - external: catalog.pick( - 'yoga-layout', - 'string-width', - 'cli-truncate', - '@xterm/xterm', - '@xterm/headless', - '@xterm/addon-fit', - '@xterm/addon-webgl', - ), + external: catalog.pick('yoga-layout', 'string-width', 'cli-truncate', '@xterm/xterm', '@xterm/headless', '@xterm/addon-fit', '@xterm/addon-webgl'), }, devDependencies: { workspace: [utilsDevPkg], external: { - ...catalog.pick( - ...peerDepNames, - '@types/node', - '@types/react', - '@types/react-reconciler', - 'vitest', - '@effect/vitest', - '@playwright/test', - 'effect', - '@effect/platform', - ...effectAtomDeps, - ...opentuiDeps, - 'storybook', - '@storybook/react', - '@storybook/react-vite', - 'vite', - '@vitejs/plugin-react', - 'typescript', - ), + ...catalog.pick(...peerDepNames, '@types/node', '@types/react', '@types/react-reconciler', 'vitest', '@effect/vitest', '@playwright/test', 'effect', ...effectAtomDeps, ...opentuiDeps, 'storybook', '@storybook/react', '@storybook/react-vite', 'vite', '@vitejs/plugin-react', 'typescript'), }, }, peerDependencies: { diff --git a/packages/@overeng/tui-react/src/components/TaskList.tsx b/packages/@overeng/tui-react/src/components/TaskList.tsx index 3f705512ce..3a075609db 100644 --- a/packages/@overeng/tui-react/src/components/TaskList.tsx +++ b/packages/@overeng/tui-react/src/components/TaskList.tsx @@ -35,7 +35,13 @@ import { Text } from './Text.tsx' // ============================================================================= /** Status of a task item */ -export const TaskStatusSchema = Schema.Literal('pending', 'active', 'success', 'error', 'skipped') +export const TaskStatusSchema = Schema.Literals([ + 'pending', + 'active', + 'success', + 'error', + 'skipped', +]) /** Inferred type for a task status literal ('pending' | 'active' | 'success' | 'error' | 'skipped'). */ export type TaskStatus = Schema.Schema.Type diff --git a/packages/@overeng/tui-react/src/effect/LogCapture.ts b/packages/@overeng/tui-react/src/effect/LogCapture.ts index a24498b39d..af80a5fba3 100644 --- a/packages/@overeng/tui-react/src/effect/LogCapture.ts +++ b/packages/@overeng/tui-react/src/effect/LogCapture.ts @@ -31,21 +31,15 @@ */ import type { Layer, Scope } from 'effect' -import { - Effect, - Fiber, - FiberId, - Inspectable, - Logger, - Runtime, - Stream, - SubscriptionRef, -} from 'effect' +import { Effect, Fiber, Inspectable, Logger, References, Stream, SubscriptionRef } from 'effect' import React, { createContext, type ReactNode } from 'react' import { useContext, useSyncExternalStore } from './hooks.tsx' import type { TuiLogEntry } from './TuiLogger.ts' +const formatFiberId = (fiberId: unknown): string => + typeof fiberId === 'number' ? `#${fiberId}` : String(fiberId ?? 'unknown') + // ============================================================================= // Types // ============================================================================= @@ -137,7 +131,9 @@ export const useCapturedLogs = (): readonly TuiLogEntry[] => { const subscribeActive = (onStoreChange: () => void): (() => void) => { if (handle === null) return () => {} const fiber = Effect.runFork( - handle.logsRef.changes.pipe(Stream.runForEach(() => Effect.sync(() => onStoreChange()))), + SubscriptionRef.changes(handle.logsRef).pipe( + Stream.runForEach(() => Effect.sync(() => onStoreChange())), + ), ) return () => { void Effect.runFork(Fiber.interrupt(fiber)) @@ -180,8 +176,6 @@ export const createLogCapture = (options?: { // Create the SubscriptionRef for log entries const logsRef = yield* SubscriptionRef.make([]) - const runtime = yield* Effect.runtime() - // Helper to append a log entry (fire and forget) const appendLog = (entry: TuiLogEntry) => SubscriptionRef.update(logsRef, (logs) => { @@ -190,27 +184,27 @@ export const createLogCapture = (options?: { }) const appendLogSync = (entry: TuiLogEntry): void => { - void Runtime.runFork(runtime)(appendLog(entry)) + void Effect.runFork(appendLog(entry)) } // Create Effect Logger that captures instead of printing - const capturingLogger = Logger.make( - ({ logLevel, message, date, fiberId, annotations, spans }) => { - const spanLabel = spans._tag === 'Cons' ? spans.head.label : undefined - const entry: TuiLogEntry = { - id: ++logCaptureEntryId, - level: logLevel.label, - message: String(message), - timestamp: date, - fiberId: FiberId.threadName(fiberId), - annotations: Object.fromEntries(annotations), - ...(spanLabel !== undefined ? { span: spanLabel } : {}), - } - appendLogSync(entry) - }, - ) + const capturingLogger = Logger.make(({ logLevel, message, date, fiber }) => { + const annotations = fiber.getRef(References.CurrentLogAnnotations) + const spans = fiber.getRef(References.CurrentLogSpans) + const spanLabel = spans[0]?.[0] + const entry: TuiLogEntry = { + id: ++logCaptureEntryId, + level: logLevel.toUpperCase(), + message: String(message), + timestamp: date, + fiberId: formatFiberId(fiber.id), + annotations: { ...annotations }, + ...(spanLabel !== undefined ? { span: spanLabel } : {}), + } + appendLogSync(entry) + }) - const loggerLayer = Logger.replace(Logger.defaultLogger, capturingLogger) + const loggerLayer = Logger.layer([capturingLogger]) // Override console methods const originalLog = console.log diff --git a/packages/@overeng/tui-react/src/effect/OpenTuiRenderer.ts b/packages/@overeng/tui-react/src/effect/OpenTuiRenderer.ts index 7783645d9d..fc13bc315f 100644 --- a/packages/@overeng/tui-react/src/effect/OpenTuiRenderer.ts +++ b/packages/@overeng/tui-react/src/effect/OpenTuiRenderer.ts @@ -35,7 +35,7 @@ import type { CliRenderer, CliRendererConfig, KeyEvent } from '@opentui/core' import type { Root } from '@opentui/react' import type { Scope, SubscriptionRef } from 'effect' -import { Effect, PubSub, Runtime } from 'effect' +import { Effect, PubSub } from 'effect' import type { FC } from 'react' import type { InputEvent } from './events.ts' @@ -242,7 +242,7 @@ export const useOpenTuiRenderer = ( // Import React for createElement const React = yield* Effect.promise(() => import('react')) - const runtime = yield* Effect.runtime() + const context = yield* Effect.context() // Create wrapper component that bridges events const WrapperComponent: FC = () => { @@ -251,7 +251,7 @@ export const useOpenTuiRenderer = ( (key) => { if (eventPubSub !== undefined) { const event = bridgeKeyEvent(key) - void Runtime.runFork(runtime)(PubSub.publish(eventPubSub, event)) + void Effect.runForkWith(context)(PubSub.publish(eventPubSub, event)) } }, { release: false }, @@ -261,7 +261,7 @@ export const useOpenTuiRenderer = ( reactLib.useOnResize((width, height) => { if (eventPubSub !== undefined) { const event = bridgeResizeEvent({ width, height }) - void Runtime.runFork(runtime)(PubSub.publish(eventPubSub, event)) + void Effect.runForkWith(context)(PubSub.publish(eventPubSub, event)) } }) diff --git a/packages/@overeng/tui-react/src/effect/OutputMode.tsx b/packages/@overeng/tui-react/src/effect/OutputMode.tsx index e0aee670ad..4f72087052 100644 --- a/packages/@overeng/tui-react/src/effect/OutputMode.tsx +++ b/packages/@overeng/tui-react/src/effect/OutputMode.tsx @@ -269,7 +269,7 @@ export const fullscreenRenderConfig = altScreenRenderConfig * }) * ``` */ -export class OutputModeTag extends Context.Tag('OutputMode')() {} +export class OutputModeTag extends Context.Service()('OutputMode') {} /** * Stream the TUI view renders into. @@ -283,10 +283,9 @@ export class OutputModeTag extends Context.Tag('OutputMode')() {} +export class ViewOutputStreamTag extends Context.Service()( + '@overeng/tui-react/ViewOutputStream', +) {} // ============================================================================= // Environment Helpers (browser-safe — use typeof process guards) diff --git a/packages/@overeng/tui-react/src/effect/TerminalInput.ts b/packages/@overeng/tui-react/src/effect/TerminalInput.ts index f811dadef4..e76f3a4a22 100644 --- a/packages/@overeng/tui-react/src/effect/TerminalInput.ts +++ b/packages/@overeng/tui-react/src/effect/TerminalInput.ts @@ -26,7 +26,7 @@ import type { Readable } from 'node:stream' -import { Effect, PubSub, Runtime, Stream } from 'effect' +import { Effect, PubSub, Stream } from 'effect' import { type KeyEvent, keyEvent, resizeEvent, type InputEvent } from './events.ts' @@ -507,7 +507,7 @@ export const createTerminalInput = Effect.fn('TerminalInput.create')(function* ( // Create event PubSub const pubsub = yield* PubSub.unbounded() - const runtime = yield* Effect.runtime() + const context = yield* Effect.context() // Track if raw mode was set let didEnableRawMode = false @@ -562,7 +562,7 @@ export const createTerminalInput = Effect.fn('TerminalInput.create')(function* ( for (const event of events) { // Publish events synchronously and let the consuming Effect decide // whether Ctrl+C should interrupt the current fiber. - void Runtime.runFork(runtime)(PubSub.publish(pubsub, event)) + void Effect.runForkWith(context)(PubSub.publish(pubsub, event)) } } @@ -578,7 +578,7 @@ export const createTerminalInput = Effect.fn('TerminalInput.create')(function* ( resizeHandler = () => { const cols = output.columns ?? 80 const rows = output.rows ?? 24 - void Runtime.runFork(runtime)(PubSub.publish(pubsub, resizeEvent({ cols, rows }))) + void Effect.runForkWith(context)(PubSub.publish(pubsub, resizeEvent({ cols, rows }))) } process.on('SIGWINCH', resizeHandler) @@ -654,7 +654,7 @@ export const createTerminalResize = Effect.fn('TerminalResize.create')(function* ) { // Create PubSub for resize events const pubsub = yield* PubSub.unbounded<{ cols: number; rows: number }>() - const runtime = yield* Effect.runtime() + const context = yield* Effect.context() // Get dimensions helper const getDimensions = (): { cols: number; rows: number } => ({ @@ -666,7 +666,7 @@ export const createTerminalResize = Effect.fn('TerminalResize.create')(function* // Set up resize handler const resizeHandler = () => { const dims = getDimensions() - void Runtime.runFork(runtime)(PubSub.publish(pubsub, dims)) + void Effect.runForkWith(context)(PubSub.publish(pubsub, dims)) } yield* Effect.acquireRelease( diff --git a/packages/@overeng/tui-react/src/effect/TuiApp.tsx b/packages/@overeng/tui-react/src/effect/TuiApp.tsx index 0f4685d3f3..0f7cb876e9 100644 --- a/packages/@overeng/tui-react/src/effect/TuiApp.tsx +++ b/packages/@overeng/tui-react/src/effect/TuiApp.tsx @@ -41,7 +41,6 @@ import { Console as NodeConsole } from 'node:console' -import { Atom, Registry } from '@effect-atom/atom' import type { Scope } from 'effect' import { Cause, @@ -53,10 +52,10 @@ import { Logger, Option, PubSub, - Runtime, Schema, Stream, } from 'effect' +import { Atom, AtomRegistry } from 'effect/unstable/reactivity' import React, { type ReactElement, type ReactNode, createContext } from 'react' import { renderToString } from '../renderToString.ts' @@ -87,21 +86,21 @@ export const isTuiApp = (u: unknown): u is TuiApp => typeof u === 'object' && u !== null && TuiAppTypeId in u /** Internal tag to skip mode-specific output when standalone run() handles output */ -const SkipModeOutputTag = Context.GenericTag('@overeng/tui-react/SkipModeOutput') +const SkipModeOutputTag = Context.Service('@overeng/tui-react/SkipModeOutput') // ============================================================================= -// TUI Registry Context (avoids multiple React instance issues with @effect-atom/atom-react) +// TUI AtomRegistry Context (avoids multiple React instance issues with @effect/atom-react) // ============================================================================= /** * Context for providing the TUI registry to components. - * Uses our own React instance to avoid context sharing issues with @effect-atom/atom-react. + * Uses our own React instance to avoid context sharing issues with @effect/atom-react. */ -export const TuiRegistryContext = createContext(null) +export const TuiRegistryContext = createContext(null) /** * Hook to get an atom's value from the TUI registry. - * This is a workaround for multiple React instance issues with @effect-atom/atom-react. + * This is a workaround for multiple React instance issues with @effect/atom-react. * * Uses useSyncExternalStore for proper React 18+ integration. * @@ -122,7 +121,7 @@ export const useTuiAtomValue = (atom: Atom.Atom): T => { // Use useSyncExternalStore for proper React integration const subscribe = useCallback( (callback: () => void) => { - // Registry.subscribe returns an unsubscribe function + // AtomRegistry.subscribe returns an unsubscribe function const unsubscribe = registry.subscribe(atom, callback) return unsubscribe }, @@ -164,13 +163,13 @@ export interface TuiAppConfig { /** * Effect Schema for state serialization (used in JSON modes). */ - readonly stateSchema: Schema.Schema + readonly stateSchema: Schema.ConstraintCodec /** * Effect Schema for action serialization (for debugging/logging). * If includes Schema.TaggedStruct('Interrupted', {}), system auto-dispatches on Ctrl+C. */ - readonly actionSchema: Schema.Schema + readonly actionSchema: Schema.ConstraintCodec /** * Initial state value. @@ -215,7 +214,7 @@ export interface TuiAppConfig { */ export interface NdjsonConfig { /** Schema for encoding output events */ - readonly eventSchema: Schema.Schema + readonly eventSchema: Schema.ConstraintCodec /** Map an action + previous state to zero or more output events */ readonly fromAction: (args: { action: A; prevState: S }) => ReadonlyArray } @@ -298,7 +297,7 @@ export interface TuiApp { /** * Check if an Effect Schema has an 'Interrupted' variant (TaggedStruct with _tag: 'Interrupted'). */ -const hasInterruptedVariant = (schema: Schema.Schema): boolean => { +const hasInterruptedVariant = (schema: Schema.ConstraintCodec): boolean => { const ast = schema.ast // Check if it's a Union @@ -308,8 +307,8 @@ const hasInterruptedVariant = (schema: Schema.Schema): boolean => { // Check each variant in the union return ast.types.some((type) => { - // We're looking for a TypeLiteral with a _tag property set to 'Interrupted' - if (type._tag !== 'TypeLiteral') { + // We're looking for an Objects node with a _tag property set to 'Interrupted' + if (type._tag !== 'Objects') { return false } @@ -332,7 +331,7 @@ const hasInterruptedVariant = (schema: Schema.Schema): boolean => { /** * Create an Interrupted action value if the schema supports it. */ -const createInterruptedAction = (schema: Schema.Schema): A | null => { +const createInterruptedAction = (schema: Schema.ConstraintCodec): A | null => { if (hasInterruptedVariant(schema) === false) { return null } @@ -354,10 +353,7 @@ const createInterruptedAction = (schema: Schema.Schema): A | null => { * ```typescript * const CounterApp = createTuiApp({ * stateSchema: Schema.Struct({ count: Schema.Number }), - * actionSchema: Schema.Union( - * Schema.TaggedStruct('Inc', {}), - * Schema.TaggedStruct('Dec', {}), - * ), + * actionSchema: Schema.Union([* Schema.TaggedStruct('Inc', {}), * Schema.TaggedStruct('Dec', {}), *]), * initial: { count: 0 }, * reducer: ({ state, action }) => { * switch (action._tag) { @@ -393,15 +389,13 @@ export const createTuiApp = (config: TuiAppConfig): TuiApp => get.set(stateAtom, newState) }) - // Create a registry for this app - const registry = Registry.make() - // Check once if schema has Interrupted variant const interruptedAction = createInterruptedAction(config.actionSchema) const run_ = ( view?: ReactElement, ): Effect.Effect, never, Scope.Scope | OutputModeTag> => Effect.gen(function* () { + const registry = AtomRegistry.make() const mode = yield* OutputModeTag const { stateSchema } = config @@ -413,8 +407,6 @@ export const createTuiApp = (config: TuiAppConfig): TuiApp => // Create action PubSub for streaming const actionPubSub = yield* PubSub.unbounded() - const runtime = yield* Effect.runtime() - // Mutable ref for NDJSON event emitter (set after setupMode when ndjson config is active) let eventEmitterRef: ((args: { action: A; prevState: S }) => void) | undefined @@ -426,7 +418,7 @@ export const createTuiApp = (config: TuiAppConfig): TuiApp => // Emit NDJSON events if configured if (eventEmitterRef !== undefined) eventEmitterRef({ action, prevState: prevState! }) // Also publish to PubSub for action stream - void Runtime.runFork(runtime)(PubSub.publish(actionPubSub, action)) + void Effect.runFork(PubSub.publish(actionPubSub, action)) } // Track root for manual unmount @@ -501,7 +493,7 @@ export const createTuiApp = (config: TuiAppConfig): TuiApp => if ( interruptedAction != null && exit._tag === 'Failure' && - Cause.isInterruptedOnly(exit.cause) === true + Cause.hasInterruptsOnly(exit.cause) === true ) { dispatch(interruptedAction) } @@ -546,8 +538,8 @@ const setupMode = ({ }: { mode: OutputMode stateAtom: Atom.Writable - stateSchema: Schema.Schema - registry: Registry.Registry + stateSchema: Schema.ConstraintCodec + registry: AtomRegistry.AtomRegistry view?: ReactElement | undefined ndjsonConfig?: NdjsonConfig | undefined }): Effect.Effect => { @@ -602,7 +594,7 @@ const setupProgressiveVisualWithView = ({ renderConfig, capturedLogs, }: { - registry: Registry.Registry + registry: AtomRegistry.AtomRegistry view: ReactElement renderConfig: RenderConfig capturedLogs?: LogCaptureHandle @@ -617,7 +609,7 @@ const setupProgressiveVisualWithView = ({ ) const root = createRoot({ terminalOrStream: viewStream }) - // Wrapper that provides Registry via our own context (avoids multiple React instance issues) + // Wrapper that provides AtomRegistry via our own context (avoids multiple React instance issues) const TuiAppWrapper = (): ReactNode => { let content: ReactNode = ( {view} @@ -652,7 +644,7 @@ const setupFinalVisualWithAtom = ({ renderConfig, }: { view: ReactElement | undefined - registry: Registry.Registry + registry: AtomRegistry.AtomRegistry renderConfig: RenderConfig }): Effect.Effect => { if (view === undefined) return Effect.void @@ -707,13 +699,13 @@ const setupFinalJsonWithAtom = ({ registry, }: { stateAtom: Atom.Writable - stateSchema: Schema.Schema - registry: Registry.Registry + stateSchema: Schema.ConstraintCodec + registry: AtomRegistry.AtomRegistry }): Effect.Effect => Effect.addFinalizer(() => Effect.gen(function* () { const finalState = registry.get(stateAtom) - const jsonString = yield* Schema.encode(Schema.parseJson(stateSchema))(finalState) + const jsonString = yield* Schema.encodeEffect(Schema.fromJsonString(stateSchema))(finalState) yield* Console.log(jsonString) }).pipe(Effect.orDie), ) @@ -731,23 +723,21 @@ const setupProgressiveJsonWithAtom = ({ registry, }: { stateAtom: Atom.Writable - stateSchema: Schema.Schema - registry: Registry.Registry + stateSchema: Schema.ConstraintCodec + registry: AtomRegistry.AtomRegistry }): Effect.Effect => Effect.gen(function* () { - const runtime = yield* Effect.runtime() - // Initial snapshot for bootstrapping. const initialState = registry.get(stateAtom) - const initialJson = yield* Schema.encode(Schema.parseJson(stateSchema))(initialState).pipe( - Effect.orDie, - ) + const initialJson = yield* Schema.encodeEffect(Schema.fromJsonString(stateSchema))( + initialState, + ).pipe(Effect.orDie) yield* Console.log(initialJson) // Subscribe to subsequent state changes. const unsubscribe = registry.subscribe(stateAtom, (state) => { - Runtime.runSync(runtime)( - Schema.encode(Schema.parseJson(stateSchema))(state).pipe( + Effect.runSync( + Schema.encodeEffect(Schema.fromJsonString(stateSchema))(state).pipe( Effect.flatMap((jsonString) => Console.log(jsonString)), Effect.orDie, ), @@ -774,25 +764,24 @@ const setupProgressiveJsonWithEvents = ({ ndjsonConfig, }: { stateAtom: Atom.Writable - stateSchema: Schema.Schema - registry: Registry.Registry + stateSchema: Schema.ConstraintCodec + registry: AtomRegistry.AtomRegistry ndjsonConfig: NdjsonConfig }): Effect.Effect<(args: { action: any; prevState: S }) => void, never, Scope.Scope> => Effect.gen(function* () { - const runtime = yield* Effect.runtime() const { eventSchema, fromAction } = ndjsonConfig const initialState = registry.get(stateAtom) - const initialJson = yield* Schema.encode(Schema.parseJson(stateSchema))(initialState).pipe( - Effect.orDie, - ) + const initialJson = yield* Schema.encodeEffect(Schema.fromJsonString(stateSchema))( + initialState, + ).pipe(Effect.orDie) yield* Console.log(initialJson) const emitter = ({ action, prevState }: { action: any; prevState: S }): void => { const events = fromAction({ action, prevState }) for (const event of events) { - Runtime.runSync(runtime)( - Schema.encode(Schema.parseJson(eventSchema))(event).pipe( + Effect.runSync( + Schema.encodeEffect(Schema.fromJsonString(eventSchema))(event).pipe( Effect.flatMap((jsonString) => Console.log(jsonString)), Effect.orDie, ), @@ -881,64 +870,25 @@ export const run: { */ export interface RunResultOptions { /** Schema for the command's result type (the handler's return value). */ - readonly result: Schema.Schema + readonly result: Schema.ConstraintCodec /** Optional React element to render in visual modes. */ readonly view?: ReactElement } /** Check if schema resolves to a plain string type */ -const isStringSchema = (schema: Schema.Schema): boolean => - schema.ast._tag === 'StringKeyword' +const isStringSchema = (schema: Schema.ConstraintCodec): boolean => + schema.ast._tag === 'String' /** * Build an Effect `Console` service bound to a single Node `WriteStream`. * * Used by `runResult` to route handler-emitted `Effect.Console.log`/`.info`/ * `.warn`/… to stderr so they don't contaminate the stdout result channel. - * Wraps Node's built-in `console.Console` (which already understands the full - * Console surface) and promotes each method into an `Effect`. + * Wraps Node's built-in `console.Console`, which implements the full Effect 4 + * Console service surface. */ -const consoleOnStream = (stream: NodeJS.WriteStream): Console.Console => { - const raw = new NodeConsole({ stdout: stream, stderr: stream }) - // Brand the object with the Console `TypeId` so `Console.setConsole` - // accepts it. The symbol is keyed as `effect/Console` via `Symbol.for`. - const TypeId = Symbol.for('effect/Console') - const service = { - [TypeId]: TypeId, - assert: (condition: boolean, ...args: ReadonlyArray) => - Effect.sync(() => raw.assert(condition, ...args)), - clear: Effect.sync(() => raw.clear()), - count: (label?: string) => Effect.sync(() => raw.count(label)), - countReset: (label?: string) => Effect.sync(() => raw.countReset(label)), - debug: (...args: ReadonlyArray) => Effect.sync(() => raw.debug(...args)), - // oxlint-disable-next-line overeng/named-args -- matches effect Console.Console interface - dir: (item: any, options?: any) => Effect.sync(() => raw.dir(item, options)), - dirxml: (...args: ReadonlyArray) => Effect.sync(() => raw.dirxml(...args)), - error: (...args: ReadonlyArray) => Effect.sync(() => raw.error(...args)), - group: (options?: { label?: string; collapsed?: boolean }) => - Effect.sync(() => - options?.collapsed === true - ? raw.groupCollapsed(options?.label) - : raw.group(options?.label), - ), - groupEnd: Effect.sync(() => raw.groupEnd()), - info: (...args: ReadonlyArray) => Effect.sync(() => raw.info(...args)), - log: (...args: ReadonlyArray) => Effect.sync(() => raw.log(...args)), - // oxlint-disable-next-line overeng/named-args -- matches effect Console.Console interface - table: (tabularData: any, properties?: ReadonlyArray) => - Effect.sync(() => - raw.table(tabularData, properties === undefined ? undefined : [...properties]), - ), - time: (label?: string) => Effect.sync(() => raw.time(label)), - timeEnd: (label?: string) => Effect.sync(() => raw.timeEnd(label)), - timeLog: (label?: string, ...args: ReadonlyArray) => - Effect.sync(() => raw.timeLog(label, ...args)), - trace: (...args: ReadonlyArray) => Effect.sync(() => raw.trace(...args)), - warn: (...args: ReadonlyArray) => Effect.sync(() => raw.warn(...args)), - unsafe: raw, - } - return service as unknown as Console.Console -} +const consoleOnStream = (stream: NodeJS.WriteStream): Console.Console => + new NodeConsole({ stdout: stream, stderr: stream }) as unknown as Console.Console /** Write a value to stdout using the appropriate format for its schema type. * Strings are written raw (no JSON encoding). Structured types are JSON-encoded. @@ -953,15 +903,15 @@ const writeResult = ({ schema, }: { value: O - schema: Schema.Schema + schema: Schema.ConstraintCodec }): Effect.Effect => - isStringSchema(schema as Schema.Schema) === true + isStringSchema(schema as Schema.ConstraintCodec) === true ? Effect.sync(() => { const str = String(value) process.stdout.write(str) if (str.length > 0 && str.endsWith('\n') === false) process.stdout.write('\n') }) - : Schema.encode(Schema.parseJson(schema))(value).pipe( + : Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe( Effect.flatMap((json) => Effect.sync(() => { process.stdout.write(json) @@ -1005,8 +955,8 @@ const runResultImpl = ( // plumbing at the main site — `runResult`'s contract is self-contained. const stderrSideChannelLayer = Layer.mergeAll( Layer.succeed(ViewOutputStreamTag, process.stderr), - Logger.replace(Logger.defaultLogger, Logger.prettyLogger().pipe(Logger.withConsoleError)), - Console.setConsole(consoleOnStream(process.stderr)), + Logger.layer([Logger.consolePretty().pipe(Logger.withConsoleError)]), + Layer.succeed(Console.Console, consoleOnStream(process.stderr)), ) const innerEffect = diff --git a/packages/@overeng/tui-react/src/effect/TuiLogger.ts b/packages/@overeng/tui-react/src/effect/TuiLogger.ts index cd677e6b7c..93612b1201 100644 --- a/packages/@overeng/tui-react/src/effect/TuiLogger.ts +++ b/packages/@overeng/tui-react/src/effect/TuiLogger.ts @@ -33,17 +33,19 @@ import type { Scope } from 'effect' import { Context, Effect, - FiberId, Layer, Logger, LogLevel, + References, Stream, SubscriptionRef, Fiber, - Runtime, } from 'effect' import { useSyncExternalStore } from 'react' +const formatFiberId = (fiberId: unknown): string => + typeof fiberId === 'number' ? `#${fiberId}` : String(fiberId ?? 'unknown') + // ============================================================================= // Types // ============================================================================= @@ -82,7 +84,7 @@ export interface TuiLoggerOptions { /** * Minimum log level to capture. * Logs below this level are not captured. - * @default LogLevel.All + * @default "All" */ minLevel?: LogLevel.LogLevel @@ -146,7 +148,7 @@ export const createTuiLogger = ( options: TuiLoggerOptions = {}, ): Effect.Effect => Effect.gen(function* () { - const { maxEntries = 100, minLevel = LogLevel.All, logToConsole = true } = options + const { maxEntries = 100, minLevel = 'All', logToConsole = true } = options // Create the logs SubscriptionRef const logsRef = yield* SubscriptionRef.make([]) @@ -158,41 +160,38 @@ export const createTuiLogger = ( // Trim to maxEntries return newLogs.length > maxEntries ? newLogs.slice(-maxEntries) : newLogs }) - - const runtime = yield* Effect.runtime() - // Create the TUI logger - const tuiLogger = Logger.make( - ({ logLevel, message, date, fiberId, annotations, spans }) => { - // Check minimum level - if (LogLevel.greaterThanEqual(logLevel, minLevel) === true) { - const spanLabel = spans._tag === 'Cons' ? spans.head.label : undefined - const entry: TuiLogEntry = { - id: ++logEntryId, - level: logLevel.label, - message: String(message), - timestamp: date, - fiberId: FiberId.threadName(fiberId), - annotations: Object.fromEntries(annotations), - ...(spanLabel !== undefined ? { span: spanLabel } : {}), - } - - // Fire and forget - we don't want logging to block - void Runtime.runFork(runtime)(appendLog(entry)) + const tuiLogger = Logger.make(({ logLevel, message, date, fiber }) => { + // Check minimum level + if (LogLevel.isGreaterThanOrEqualTo(logLevel, minLevel) === true) { + const annotations = fiber.getRef(References.CurrentLogAnnotations) + const spans = fiber.getRef(References.CurrentLogSpans) + const spanLabel = spans[0]?.[0] + const entry: TuiLogEntry = { + id: ++logEntryId, + level: logLevel.toUpperCase(), + message: String(message), + timestamp: date, + fiberId: formatFiberId(fiber.id), + annotations: { ...annotations }, + ...(spanLabel !== undefined ? { span: spanLabel } : {}), } - }, - ) + + // Fire and forget - we don't want logging to block + void Effect.runFork(appendLog(entry)) + } + }) // Create the layer - either TUI only or combined with console const layer = logToConsole === true ? Layer.merge( - Logger.replace(Logger.defaultLogger, Logger.zip(Logger.defaultLogger, tuiLogger)), - Logger.minimumLogLevel(minLevel), + Logger.layer([Logger.defaultLogger, tuiLogger]), + Layer.succeed(References.MinimumLogLevel, minLevel), ) : Layer.merge( - Logger.replace(Logger.defaultLogger, tuiLogger), - Logger.minimumLogLevel(minLevel), + Logger.layer([tuiLogger]), + Layer.succeed(References.MinimumLogLevel, minLevel), ) // Clear function @@ -251,7 +250,7 @@ export const useTuiLogs = ( // Subscribe to changes const subscribe = (onStoreChange: () => void): (() => void) => { const fiber = Effect.runFork( - logsRef.changes.pipe( + SubscriptionRef.changes(logsRef).pipe( Stream.runForEach(() => Effect.sync(() => { onStoreChange() @@ -276,10 +275,9 @@ export const useTuiLogs = ( * Service tag for TUI Logger result. * Use this when you want to inject the logger via the Effect context. */ -export class TuiLoggerService extends Context.Tag('TuiLogger')< - TuiLoggerService, - TuiLoggerResult ->() {} +export class TuiLoggerService extends Context.Service()( + 'TuiLogger', +) {} /** * Create a layer that provides TuiLoggerService. @@ -294,7 +292,7 @@ export class TuiLoggerService extends Context.Tag('TuiLogger')< */ export const TuiLoggerServiceLayer = ( options: TuiLoggerOptions = {}, -): Layer.Layer => Layer.scoped(TuiLoggerService, createTuiLogger(options)) +): Layer.Layer => Layer.effect(TuiLoggerService, createTuiLogger(options)) // ============================================================================= // Utility Functions diff --git a/packages/@overeng/tui-react/src/effect/TuiRenderer.ts b/packages/@overeng/tui-react/src/effect/TuiRenderer.ts index ff5bdddb52..8213940911 100644 --- a/packages/@overeng/tui-react/src/effect/TuiRenderer.ts +++ b/packages/@overeng/tui-react/src/effect/TuiRenderer.ts @@ -80,7 +80,7 @@ export interface TuiRendererService { * }) * ``` */ -export class TuiRenderer extends Context.Tag('TuiRenderer')() { +export class TuiRenderer extends Context.Service()('TuiRenderer') { /** * Render a React element to the terminal. */ @@ -110,7 +110,7 @@ export class TuiRenderer extends Context.Tag('TuiRenderer') => - Layer.scoped( + Layer.effect( TuiRenderer, Effect.gen(function* () { const target = terminal ?? process.stdout diff --git a/packages/@overeng/tui-react/src/effect/cli.tsx b/packages/@overeng/tui-react/src/effect/cli.tsx index 042ca0f917..10a2f0869a 100644 --- a/packages/@overeng/tui-react/src/effect/cli.tsx +++ b/packages/@overeng/tui-react/src/effect/cli.tsx @@ -6,12 +6,12 @@ * * @example * ```typescript - * import { Command, Options } from "@effect/cli" + * import { Command, Flag as Options } from "effect/unstable/cli" * import { Effect } from "effect" * import { outputOption, outputModeLayer } from "@overeng/tui-react" * * const myCommand = Command.make("my-cmd", { - * name: Options.text("name"), + * name: Options.string("name"), * output: outputOption, * }, ({ name, output }) => * myProgram(name).pipe( @@ -23,8 +23,8 @@ * @module */ -import { Options } from '@effect/cli' import { Cause, Effect, Exit, Layer, Logger, Option } from 'effect' +import { Flag as Options } from 'effect/unstable/cli' import { createLogCapture } from './LogCapture.ts' import { detectOutputMode, viewOutputStreamStdoutLayer } from './OutputMode.node.ts' @@ -121,10 +121,9 @@ const modeMap: Record, OutputMode> = { * This is used in JSON modes to ensure all log output goes to stderr, * keeping stdout clean for JSON data only. */ -const stderrLoggerLayer: Layer.Layer = Logger.replace( - Logger.defaultLogger, - Logger.prettyLogger().pipe(Logger.withConsoleError), -) +const stderrLoggerLayer: Layer.Layer = Logger.layer([ + Logger.consolePretty().pipe(Logger.withConsoleError), +]) /** * Create an OutputMode layer from the `--output` flag value. @@ -154,7 +153,7 @@ export const outputModeLayer = (value: OutputModeValue): Layer.Layer - * Cause.failures(cause).pipe(chunk => - * [...chunk].some(e => e._tag === 'SyncFailedError') + * cause.reasons.some( + * reason => Cause.isFailReason(reason) && reason.error._tag === 'SyncFailedError' * ) ? Option.none() * : defaultFormatError(cause) * ``` @@ -236,16 +235,16 @@ export interface RunTuiMainOptions { /** Verbose error formatter — full stack traces via `Cause.pretty`. This is the default. */ export const defaultFormatError = (cause: Cause.Cause): Option.Option => - Option.some(Cause.pretty(cause, { renderErrorCause: true })) + Option.some(Cause.pretty(cause)) /** Compact error formatter — just error messages, no stack traces. For machine/agent output. */ export const compactFormatError = (cause: Cause.Cause): Option.Option => { const messages: string[] = [] - for (const failure of Cause.failures(cause)) { - messages.push(failure instanceof Error ? failure.message : String(failure)) + for (const reason of cause.reasons.filter(Cause.isFailReason)) { + messages.push(reason.error instanceof Error ? reason.error.message : String(reason.error)) } - for (const defect of Cause.defects(cause)) { - messages.push(defect instanceof Error ? defect.message : String(defect)) + for (const reason of cause.reasons.filter(Cause.isDieReason)) { + messages.push(reason.defect instanceof Error ? reason.defect.message : String(reason.defect)) } return messages.length > 0 ? Option.some(messages.join('\n')) : Option.none() } @@ -263,7 +262,7 @@ export interface TuiRuntime { /** * Custom finalization so a signal-interrupted CLI (Ctrl-C) exits 130 — the shell - * convention — instead of the default teardown's hardcoded 0. The `catchAllCause` + * convention — instead of the default teardown's hardcoded 0. The `catchCause` * in {@link runTuiMainImpl} catches the interrupt, suppresses the stack, sets * `process.exitCode = 130`, and completes the fiber as a Success, so the * interrupt surfaces here only via `process.exitCode` — which we honor instead @@ -271,7 +270,7 @@ export interface TuiRuntime { */ // oxlint-disable-next-line overeng/named-args -- implements Effect's `Teardown` interface (fixed `(exit, onExit)` signature) passed to `NodeRuntime.runMain` const tuiTeardown = (exit: Exit.Exit, onExit: (code: number) => void): void => { - if (Exit.isFailure(exit) === true && Cause.isInterruptedOnly(exit.cause) === false) { + if (Exit.isFailure(exit) === true && Cause.hasInterruptsOnly(exit.cause) === false) { onExit(1) return } @@ -292,7 +291,7 @@ const tuiTeardown = (exit: Exit.Exit, onExit: (code: number) => void * @example * ```typescript * // Pipeable usage (recommended): - * Cli.Command.run(myCommand, { name: 'my-cli', version })(process.argv).pipe( + * Cli.Command.runWith(myCommand, { version })(process.argv.slice(2)).pipe( * Effect.scoped, * Effect.provide(baseLayer), * runTuiMain(NodeRuntime) @@ -308,7 +307,7 @@ const tuiTeardown = (exit: Exit.Exit, onExit: (code: number) => void * @example * ```typescript * // With compact errors (for agent-friendly CLIs): - * Cli.Command.run(myCommand, { name: 'my-cli', version })(process.argv).pipe( + * Cli.Command.runWith(myCommand, { version })(process.argv.slice(2)).pipe( * Effect.scoped, * Effect.provide(baseLayer), * runTuiMain(NodeRuntime, { formatError: compactFormatError }) @@ -361,9 +360,9 @@ const runTuiMainImpl = ({ const formatError = options?.formatError ?? defaultFormatError effect.pipe( - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.sync(() => { - if (Cause.isInterruptedOnly(cause) === true) { + if (Cause.hasInterruptsOnly(cause) === true) { process.exitCode = 130 return undefined } diff --git a/packages/@overeng/tui-react/src/effect/errors.tsx b/packages/@overeng/tui-react/src/effect/errors.tsx index bcfc1ce7a9..a2ff689b65 100644 --- a/packages/@overeng/tui-react/src/effect/errors.tsx +++ b/packages/@overeng/tui-react/src/effect/errors.tsx @@ -48,7 +48,7 @@ export type RuntimeError = Schema.Schema.Type * Cancelled error - for user cancellation, timeout, or signal. */ export const CancelledError = Schema.TaggedStruct('CommandError.Cancelled', { - reason: Schema.Literal('user', 'timeout', 'signal'), + reason: Schema.Literals(['user', 'timeout', 'signal']), }) /** Inferred type for a cancellation error (user, timeout, or signal). */ export type CancelledError = Schema.Schema.Type @@ -56,7 +56,7 @@ export type CancelledError = Schema.Schema.Type /** * Union of all command error types. */ -export const CommandError = Schema.Union(ValidationError, RuntimeError, CancelledError) +export const CommandError = Schema.Union([ValidationError, RuntimeError, CancelledError]) /** Inferred union type of all command error variants. */ export type CommandError = Schema.Schema.Type @@ -111,7 +111,7 @@ export const cancelledError = (reason: 'user' | 'timeout' | 'signal'): Cancelled * Used in JSON mode to maintain modal consistency. */ export const outputJsonError = (error: CommandError): Effect.Effect => - Schema.encode(Schema.parseJson(CommandError))(error).pipe( + Schema.encodeEffect(Schema.fromJsonString(CommandError))(error).pipe( Effect.flatMap((jsonString) => Console.log(jsonString)), Effect.orDie, // Schema encoding of our own types should never fail ) @@ -160,7 +160,7 @@ export const withJsonErrors = ( if (isJson(mode) === true) { // In JSON mode, catch errors and output as JSON const result = yield* effect.pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* outputJsonError(toCommandError(error)) return undefined as unknown as A @@ -187,7 +187,7 @@ export const runWithJsonErrors = ( ): Effect.Effect => effect.pipe( withJsonErrors, - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { const mode = yield* OutputModeTag if (isJson(mode) === true) { diff --git a/packages/@overeng/tui-react/src/effect/events.ts b/packages/@overeng/tui-react/src/effect/events.ts index f6306e4145..73e9b7badf 100644 --- a/packages/@overeng/tui-react/src/effect/events.ts +++ b/packages/@overeng/tui-react/src/effect/events.ts @@ -21,7 +21,7 @@ * ``` */ -import { Schema } from 'effect' +import { Effect, Schema } from 'effect' // ============================================================================= // Key Event @@ -73,25 +73,37 @@ export const KeyEvent = Schema.TaggedStruct('Event.Key', { * Whether the Ctrl key was held. * @default false */ - ctrl: Schema.optionalWith(Schema.Boolean, { default: () => false }), + ctrl: Schema.Boolean.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => false)), + Schema.withConstructorDefault(Effect.sync(() => false)), + ), /** * Whether the Alt/Option key was held. * @default false */ - alt: Schema.optionalWith(Schema.Boolean, { default: () => false }), + alt: Schema.Boolean.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => false)), + Schema.withConstructorDefault(Effect.sync(() => false)), + ), /** * Whether the Shift key was held. * @default false */ - shift: Schema.optionalWith(Schema.Boolean, { default: () => false }), + shift: Schema.Boolean.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => false)), + Schema.withConstructorDefault(Effect.sync(() => false)), + ), /** * Whether the Meta/Command key was held (macOS). * @default false */ - meta: Schema.optionalWith(Schema.Boolean, { default: () => false }), + meta: Schema.Boolean.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => false)), + Schema.withConstructorDefault(Effect.sync(() => false)), + ), }) /** @@ -102,7 +114,7 @@ export type KeyEvent = Schema.Schema.Type /** * Encoded type for KeyEvent (for JSON) */ -export type KeyEventEncoded = Schema.Schema.Encoded +export type KeyEventEncoded = Schema.Codec.Encoded // ============================================================================= // Resize Event @@ -142,7 +154,7 @@ export type ResizeEvent = Schema.Schema.Type /** * Encoded type for ResizeEvent (for JSON) */ -export type ResizeEventEncoded = Schema.Schema.Encoded +export type ResizeEventEncoded = Schema.Codec.Encoded // ============================================================================= // Focus Event @@ -175,7 +187,7 @@ export type FocusEvent = Schema.Schema.Type /** * Encoded type for FocusEvent (for JSON) */ -export type FocusEventEncoded = Schema.Schema.Encoded +export type FocusEventEncoded = Schema.Codec.Encoded // ============================================================================= // Mouse Event (for future use) @@ -184,7 +196,7 @@ export type FocusEventEncoded = Schema.Schema.Encoded /** * Mouse button identifier */ -export const MouseButton = Schema.Literal('left', 'middle', 'right', 'wheelUp', 'wheelDown') +export const MouseButton = Schema.Literals(['left', 'middle', 'right', 'wheelUp', 'wheelDown']) /** * Type for MouseButton @@ -212,7 +224,7 @@ export const MouseEvent = Schema.TaggedStruct('Event.Mouse', { /** * The type of mouse action */ - action: Schema.Literal('press', 'release', 'move'), + action: Schema.Literals(['press', 'release', 'move']), /** * The button involved (if any) @@ -232,17 +244,26 @@ export const MouseEvent = Schema.TaggedStruct('Event.Mouse', { /** * Whether Ctrl was held */ - ctrl: Schema.optionalWith(Schema.Boolean, { default: () => false }), + ctrl: Schema.Boolean.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => false)), + Schema.withConstructorDefault(Effect.sync(() => false)), + ), /** * Whether Alt was held */ - alt: Schema.optionalWith(Schema.Boolean, { default: () => false }), + alt: Schema.Boolean.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => false)), + Schema.withConstructorDefault(Effect.sync(() => false)), + ), /** * Whether Shift was held */ - shift: Schema.optionalWith(Schema.Boolean, { default: () => false }), + shift: Schema.Boolean.pipe( + Schema.withDecodingDefaultType(Effect.sync(() => false)), + Schema.withConstructorDefault(Effect.sync(() => false)), + ), }) /** @@ -253,7 +274,7 @@ export type MouseEvent = Schema.Schema.Type /** * Encoded type for MouseEvent (for JSON) */ -export type MouseEventEncoded = Schema.Schema.Encoded +export type MouseEventEncoded = Schema.Codec.Encoded // ============================================================================= // Input Event Union @@ -281,7 +302,7 @@ export type MouseEventEncoded = Schema.Schema.Encoded * ) * ``` */ -export const InputEvent = Schema.Union(KeyEvent, ResizeEvent, FocusEvent, MouseEvent) +export const InputEvent = Schema.Union([KeyEvent, ResizeEvent, FocusEvent, MouseEvent]) /** * Type for InputEvent @@ -291,7 +312,7 @@ export type InputEvent = Schema.Schema.Type /** * Encoded type for InputEvent (for JSON) */ -export type InputEventEncoded = Schema.Schema.Encoded +export type InputEventEncoded = Schema.Codec.Encoded // ============================================================================= // Helper Functions diff --git a/packages/@overeng/tui-react/src/effect/hooks.tsx b/packages/@overeng/tui-react/src/effect/hooks.tsx index 6a02f5fccf..54dbb2d8cb 100644 --- a/packages/@overeng/tui-react/src/effect/hooks.tsx +++ b/packages/@overeng/tui-react/src/effect/hooks.tsx @@ -51,7 +51,7 @@ export { useAtomInitialValues, RegistryProvider, RegistryContext, -} from '@effect-atom/atom-react' +} from '@effect/atom-react' // ============================================================================= // React hook re-exports (to ensure single React instance) @@ -89,7 +89,7 @@ export { // TUI-specific utilities // ============================================================================= -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' /** * Create a pair of atoms for reducer-style state management. diff --git a/packages/@overeng/tui-react/src/effect/opentui/hooks.tsx b/packages/@overeng/tui-react/src/effect/opentui/hooks.tsx index 5f59fd26f1..90920a508d 100644 --- a/packages/@overeng/tui-react/src/effect/opentui/hooks.tsx +++ b/packages/@overeng/tui-react/src/effect/opentui/hooks.tsx @@ -59,7 +59,7 @@ export const useOState = (ref: SubscriptionRef.SubscriptionRef): S => { useEffect(() => { // Subscribe to changes const fiber = Effect.runFork( - ref.changes.pipe(Stream.runForEach((s) => Effect.sync(() => setState(s)))), + SubscriptionRef.changes(ref).pipe(Stream.runForEach((s) => Effect.sync(() => setState(s)))), ) return () => { diff --git a/packages/@overeng/tui-react/src/effect/testing.tsx b/packages/@overeng/tui-react/src/effect/testing.tsx index 11703adda5..bf95af257d 100644 --- a/packages/@overeng/tui-react/src/effect/testing.tsx +++ b/packages/@overeng/tui-react/src/effect/testing.tsx @@ -19,9 +19,9 @@ * ``` */ -import { Atom, Registry } from '@effect-atom/atom' import type { Scope } from 'effect' -import { Effect, type Exit, Layer, PubSub, Runtime, Schema, Stream } from 'effect' +import { Console, Effect, Exit, Layer, PubSub, Schema, Stream } from 'effect' +import { Atom, AtomRegistry } from 'effect/unstable/reactivity' import { type OutputMode, @@ -53,7 +53,7 @@ export interface RunTestCommandOptions { /** Output mode preset to use */ mode: TestModePreset /** Schema for parsing and validating JSON output */ - schema: Schema.Schema + schema: Schema.ConstraintCodec } /** @@ -108,7 +108,10 @@ export const modeFromTag = (preset: TestModePreset): OutputMode => { * Create a layer for a specific output mode. */ export const testModeLayer = (preset: TestModePreset): Layer.Layer => - Layer.succeed(OutputModeTag, modeFromTag(preset)) + Layer.mergeAll( + Layer.succeed(OutputModeTag, modeFromTag(preset)), + Layer.succeed(Console.Console, globalThis.console), + ) /** * Run a command function with test utilities. @@ -152,11 +155,11 @@ export const runTestCommand = async ({ } // Parse and validate JSON output using schema - const jsonSchema = Schema.parseJson(options.schema) + const jsonSchema = Schema.fromJsonString(options.schema) const parsedStates = jsonOutput .map((line) => { - const result = Schema.decodeUnknownEither(jsonSchema)(line) - return result._tag === 'Right' ? result.right : null + const result = Schema.decodeUnknownExit(jsonSchema)(line) + return Exit.isSuccess(result) === true ? result.value : null }) .filter((s): s is S => s !== null) @@ -214,10 +217,10 @@ export const createTestTuiState = ( const newState = reducer({ state: currentState, action }) get.set(stateAtom, newState) }) - const registry = Registry.make() + const registry = AtomRegistry.make() const actionPubSub = yield* PubSub.unbounded() - const runtime = yield* Effect.runtime() + const context = yield* Effect.context() // Create sync dispatch function that captures states and actions directly const dispatch = (action: A): void => { @@ -229,7 +232,7 @@ export const createTestTuiState = ( // Capture the action actions.push(action) // Also publish to PubSub for the actions stream - void Runtime.runFork(runtime)(PubSub.publish(actionPubSub, action)) + void Effect.runForkWith(context)(PubSub.publish(actionPubSub, action)) } const api: TuiAppApi = { @@ -312,9 +315,9 @@ export const assertJsonMatchesSchema = ({ schema, }: { jsonString: string - schema: Schema.Schema + schema: Schema.ConstraintCodec }): S => { - return Schema.decodeSync(Schema.parseJson(schema))(jsonString) + return Schema.decodeSync(Schema.fromJsonString(schema))(jsonString) } /** diff --git a/packages/@overeng/tui-react/src/storybook/TuiStoryPreview.tsx b/packages/@overeng/tui-react/src/storybook/TuiStoryPreview.tsx index b442fbc5cc..73ced45add 100644 --- a/packages/@overeng/tui-react/src/storybook/TuiStoryPreview.tsx +++ b/packages/@overeng/tui-react/src/storybook/TuiStoryPreview.tsx @@ -37,11 +37,11 @@ // oxlint-disable-next-line typescript-eslint(triple-slash-reference) -- intentional: an ambient-only .d.ts cannot be an ES import /// -import { Atom, Registry } from '@effect-atom/atom' import { FitAddon } from '@xterm/addon-fit' import { WebglAddon } from '@xterm/addon-webgl' import { Terminal } from '@xterm/xterm' import { Schema } from 'effect' +import { Atom, AtomRegistry } from 'effect/unstable/reactivity' // oxlint-disable-next-line eslint-plugin-import(no-unassigned-import) -- deliberate bundler stylesheet import '@xterm/xterm/css/xterm.css' import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react' @@ -82,8 +82,8 @@ export interface TuiStoryPreviewProps { /** A TuiApp instance (or any object with a compatible config) */ app: { config: { - stateSchema: Schema.Schema - actionSchema: Schema.Schema + stateSchema: Schema.ConstraintCodec + actionSchema: Schema.ConstraintCodec initial: S reducer: (args: { state: S; action: A }) => S } @@ -140,9 +140,9 @@ export const TuiStoryPreview = ({ // Atom-based state management — atom is derived from initialState so it // resets when props change (e.g. Storybook controls toggling verbose/force) - const registryRef = useRef(null) + const registryRef = useRef(null) if (registryRef.current === null) { - registryRef.current = Registry.make() + registryRef.current = AtomRegistry.make() } const registry = registryRef.current const stateAtom = useMemo(() => Atom.make(initialState), [initialState]) @@ -1027,7 +1027,7 @@ const NdjsonPreviewPane: React.FC<{ lines: NdjsonLine[] }> = ({ lines }) => ( const CIPreviewPane: React.FC<{ View: React.ComponentType<{ stateAtom: Atom.Atom }> stateAtom: Atom.Atom - registry: Registry.Registry + registry: AtomRegistry.AtomRegistry height: number }> = ({ View, stateAtom, registry, height }) => { const containerRef = useRef(null) @@ -1141,7 +1141,7 @@ const useContainerColumns = (containerRef: React.RefObject): const LogPreviewPane: React.FC<{ View: React.ComponentType<{ stateAtom: Atom.Atom }> stateAtom: Atom.Atom - registry: Registry.Registry + registry: AtomRegistry.AtomRegistry height: number }> = ({ View, stateAtom, registry, height }) => { const [output, setOutput] = useState('') @@ -1193,7 +1193,7 @@ const LogPreviewPane: React.FC<{ const CIPlainPreviewPane: React.FC<{ View: React.ComponentType<{ stateAtom: Atom.Atom }> stateAtom: Atom.Atom - registry: Registry.Registry + registry: AtomRegistry.AtomRegistry height: number }> = ({ View, stateAtom, registry, height }) => { const [output, setOutput] = useState('') @@ -1245,7 +1245,7 @@ const CIPlainPreviewPane: React.FC<{ const FullscreenPreviewPane: React.FC<{ View: React.ComponentType<{ stateAtom: Atom.Atom }> stateAtom: Atom.Atom - registry: Registry.Registry + registry: AtomRegistry.AtomRegistry height: number }> = ({ View, stateAtom, registry, height }) => { const containerRef = useRef(null) diff --git a/packages/@overeng/tui-react/test/integration/cli-integration.test.tsx b/packages/@overeng/tui-react/test/integration/cli-integration.test.tsx index 53108380c9..461544ceaa 100644 --- a/packages/@overeng/tui-react/test/integration/cli-integration.test.tsx +++ b/packages/@overeng/tui-react/test/integration/cli-integration.test.tsx @@ -13,20 +13,13 @@ import { detectOutputMode } from '../../src/effect/OutputMode.node.ts' import { createTuiApp, run, useTuiAtomValue, Box, Text, testModeLayer } from '../../src/mod.tsx' const parseJson = (json: string) => - Schema.decodeSync( - Schema.parseJson( - Schema.Record({ - key: Schema.String, - value: Schema.Unknown, - }), - ), - )(json) + Schema.decodeSync(Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)))(json) // ============================================================================= // Test State Schema (simulating a deploy command) // ============================================================================= -const DeployState = Schema.Union( +const DeployState = Schema.Union([ Schema.TaggedStruct('Idle', {}), Schema.TaggedStruct('Validating', { logs: Schema.Array(Schema.String), @@ -35,7 +28,7 @@ const DeployState = Schema.Union( services: Schema.Array( Schema.Struct({ name: Schema.String, - status: Schema.Literal('pending', 'deploying', 'healthy'), + status: Schema.Literals(['pending', 'deploying', 'healthy']), }), ), logs: Schema.Array(Schema.String), @@ -44,13 +37,13 @@ const DeployState = Schema.Union( services: Schema.Array( Schema.Struct({ name: Schema.String, - result: Schema.Literal('updated', 'unchanged'), + result: Schema.Literals(['updated', 'unchanged']), duration: Schema.Number, }), ), totalDuration: Schema.Number, }), -) +]) type DeployState = Schema.Schema.Type @@ -58,32 +51,32 @@ type DeployState = Schema.Schema.Type // Action Schema // ============================================================================= -const DeployAction = Schema.Union( +const DeployAction = Schema.Union([ Schema.TaggedStruct('StartValidation', { logs: Schema.Array(Schema.String) }), Schema.TaggedStruct('StartProgress', { services: Schema.Array( Schema.Struct({ name: Schema.String, - status: Schema.Literal('pending', 'deploying', 'healthy'), + status: Schema.Literals(['pending', 'deploying', 'healthy']), }), ), logs: Schema.Array(Schema.String), }), Schema.TaggedStruct('UpdateService', { index: Schema.Number, - status: Schema.Literal('pending', 'deploying', 'healthy'), + status: Schema.Literals(['pending', 'deploying', 'healthy']), }), Schema.TaggedStruct('Complete', { services: Schema.Array( Schema.Struct({ name: Schema.String, - result: Schema.Literal('updated', 'unchanged'), + result: Schema.Literals(['updated', 'unchanged']), duration: Schema.Number, }), ), totalDuration: Schema.Number, }), -) +]) type DeployAction = Schema.Schema.Type diff --git a/packages/@overeng/tui-react/test/integration/fixtures/runResult-cli.tsx b/packages/@overeng/tui-react/test/integration/fixtures/runResult-cli.tsx index 3f91004a24..f321386064 100644 --- a/packages/@overeng/tui-react/test/integration/fixtures/runResult-cli.tsx +++ b/packages/@overeng/tui-react/test/integration/fixtures/runResult-cli.tsx @@ -22,13 +22,13 @@ import { } from '../../../src/mod.tsx' import { runTuiMain, tuiRuntimeLayer } from '../../../src/node/mod.ts' -const State = Schema.Union( +const State = Schema.Union([ Schema.TaggedStruct('Idle', {}), Schema.TaggedStruct('Approved', { output: Schema.String }), -) +]) type State = typeof State.Type -const Action = Schema.Union(Schema.TaggedStruct('Approve', { output: Schema.String })) +const Action = Schema.Union([Schema.TaggedStruct('Approve', { output: Schema.String })]) type Action = typeof Action.Type const App: TuiApp = createTuiApp({ diff --git a/packages/@overeng/tui-react/test/unit/LogCapture.test.tsx b/packages/@overeng/tui-react/test/unit/LogCapture.test.tsx index df793afd4b..6b5ac05bff 100644 --- a/packages/@overeng/tui-react/test/unit/LogCapture.test.tsx +++ b/packages/@overeng/tui-react/test/unit/LogCapture.test.tsx @@ -3,7 +3,7 @@ */ import { it } from '@effect/vitest' -import { Chunk, Effect, Fiber, Stream } from 'effect' +import { Effect, Fiber, Stream, SubscriptionRef } from 'effect' import { describe, expect, beforeEach, afterEach } from 'vitest' import { createLogCapture } from '../../src/effect/LogCapture.ts' @@ -20,11 +20,11 @@ const awaitLogs = ( handle: LogCaptureHandle, predicate: (logs: readonly TuiLogEntry[]) => boolean, ): Effect.Effect => - handle.logsRef.changes.pipe( + SubscriptionRef.changes(handle.logsRef).pipe( Stream.filter(predicate), Stream.take(1), Stream.runCollect, - Effect.map((chunk) => Chunk.unsafeGet(chunk, 0)), + Effect.map((items) => items[0]!), ) // ============================================================================= @@ -59,7 +59,7 @@ describe('createLogCapture', () => { Effect.gen(function* () { const { handle, loggerLayer } = yield* createLogCapture() - const fiber = yield* Effect.fork(awaitLogs(handle, (logs) => logs.length >= 1)) + const fiber = yield* Effect.forkChild(awaitLogs(handle, (logs) => logs.length >= 1)) yield* Effect.log('hello from effect').pipe(Effect.provide(loggerLayer)) @@ -74,7 +74,7 @@ describe('createLogCapture', () => { Effect.gen(function* () { const { handle } = yield* createLogCapture() - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( awaitLogs(handle, (logs) => logs.some((l) => l.message === 'hello from console')), ) @@ -89,7 +89,7 @@ describe('createLogCapture', () => { Effect.gen(function* () { const { handle } = yield* createLogCapture() - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( awaitLogs(handle, (logs) => logs.some((l) => l.message === 'error message')), ) @@ -106,7 +106,7 @@ describe('createLogCapture', () => { Effect.gen(function* () { const { handle } = yield* createLogCapture() - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( awaitLogs(handle, (logs) => logs.some((l) => l.message === 'warning message')), ) @@ -123,7 +123,7 @@ describe('createLogCapture', () => { Effect.gen(function* () { const { handle } = yield* createLogCapture() - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( awaitLogs( handle, (logs) => @@ -160,7 +160,7 @@ describe('createLogCapture', () => { Effect.gen(function* () { const { handle } = yield* createLogCapture({ maxEntries: 3 }) - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( awaitLogs(handle, (logs) => logs.some((l) => l.message === 'five')), ) @@ -191,7 +191,7 @@ describe('createLogCapture', () => { capturedConsoleLog(...args) } - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( awaitLogs(handle, (logs) => logs.some((l) => l.message === 'should not print')), ) @@ -199,7 +199,7 @@ describe('createLogCapture', () => { yield* Fiber.join(fiber) }).pipe( Effect.scoped, - Effect.andThen(() => { + Effect.map(() => { // The Effect.log message should not appear as direct stdout output expect(printed.filter((p) => p.includes('should not print'))).toHaveLength(0) }), @@ -232,7 +232,7 @@ describe('log capture integration', () => { const Schema = yield* Effect.promise(() => import('effect').then((m) => m.Schema)) const App = createTuiApp({ stateSchema: Schema.Struct({ count: Schema.Number }), - actionSchema: Schema.Union(Schema.TaggedStruct('Inc', {})), + actionSchema: Schema.Union([Schema.TaggedStruct('Inc', {})]), initial: { count: 0 }, reducer: ({ state, action }) => { switch (action._tag) { @@ -247,7 +247,7 @@ describe('log capture integration', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('json')), - Effect.andThen(() => { + Effect.map(() => { // JSON mode should still output to console.log (our captured output) expect(capturedOutput).toHaveLength(1) const state = JSON.parse(capturedOutput[0]!) diff --git a/packages/@overeng/tui-react/test/unit/TuiApp.test.tsx b/packages/@overeng/tui-react/test/unit/TuiApp.test.tsx index ccbdc8add2..0a7858ba99 100644 --- a/packages/@overeng/tui-react/test/unit/TuiApp.test.tsx +++ b/packages/@overeng/tui-react/test/unit/TuiApp.test.tsx @@ -10,8 +10,8 @@ import { describe, expect, beforeEach, afterEach, test } from 'vitest' import { testModeLayer } from '../../src/effect/testing.tsx' import { createTuiApp, run, runResult, useTuiAtomValue, Box, Text } from '../../src/mod.tsx' -const decodeJson = (schema: Schema.Schema, json: string): A => - Schema.decodeSync(Schema.parseJson(schema))(json) +const decodeJson = (schema: Schema.ConstraintCodec, json: string): A => + Schema.decodeSync(Schema.fromJsonString(schema))(json) // ============================================================================= // Test State and Actions @@ -23,11 +23,11 @@ const CounterState = Schema.Struct({ type CounterState = Schema.Schema.Type -const CounterAction = Schema.Union( +const CounterAction = Schema.Union([ Schema.TaggedStruct('Increment', {}), Schema.TaggedStruct('Decrement', {}), Schema.TaggedStruct('Set', { value: Schema.Number }), -) +]) type CounterAction = Schema.Schema.Type @@ -132,7 +132,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('json')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput).toHaveLength(1) const state = decodeJson(CounterState, capturedOutput[0]!) expect(state).toEqual({ count: 2 }) @@ -147,7 +147,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('json')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput).toHaveLength(1) const state = decodeJson(CounterState, capturedOutput[0]!) expect(state).toEqual({ count: 100 }) @@ -168,7 +168,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('ndjson')), - Effect.andThen(() => { + Effect.map(() => { // Initial snapshot + one line per state change. No trailing envelope. expect(capturedOutput.length).toBeGreaterThanOrEqual(2) @@ -184,11 +184,11 @@ describe('createTuiApp', () => { }) describe('ndjson mode with event mapping', () => { - const CounterEvent = Schema.Union( + const CounterEvent = Schema.Union([ Schema.TaggedStruct('Incremented', { newCount: Schema.Number }), Schema.TaggedStruct('Decremented', { newCount: Schema.Number }), Schema.TaggedStruct('Reset', { from: Schema.Number, to: Schema.Number }), - ) + ]) const EventCounterApp = createTuiApp({ stateSchema: CounterState, @@ -222,7 +222,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('ndjson')), - Effect.andThen(() => { + Effect.map(() => { // Line 1: initial full state snapshot const initialState = decodeJson(CounterState, capturedOutput[0]!) expect(initialState).toEqual({ count: 0 }) @@ -260,7 +260,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('ndjson')), - Effect.andThen(() => { + Effect.map(() => { // Only initial state snapshot — no intermediate events, no envelope. expect(capturedOutput).toHaveLength(1) expect(decodeJson(CounterState, capturedOutput[0]!)).toEqual({ count: 0 }) @@ -294,7 +294,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('ndjson')), - Effect.andThen(() => { + Effect.map(() => { // Initial + 2 events from 1 action. No trailing envelope. expect(capturedOutput).toHaveLength(3) expect(decodeJson(CounterEvent, capturedOutput[1]!)).toEqual({ @@ -365,14 +365,14 @@ describe('createTuiApp', () => { describe('Ctrl+C interruption', () => { test('dispatches Interrupted when the run fiber is interrupted', async () => { const InterruptState = Schema.TaggedStruct('InterruptState', { - status: Schema.Literal('idle', 'running', 'interrupted'), + status: Schema.Literals(['idle', 'running', 'interrupted']), }) type InterruptState = typeof InterruptState.Type - const InterruptAction = Schema.Union( + const InterruptAction = Schema.Union([ Schema.TaggedStruct('Start', {}), Schema.TaggedStruct('Interrupted', {}), - ) + ]) type InterruptAction = typeof InterruptAction.Type const previousExitCode = process.exitCode @@ -408,7 +408,7 @@ describe('createTuiApp', () => { expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit) === true) { - expect(Cause.isInterruptedOnly(exit.cause)).toBe(true) + expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true) } expect(reducedStatuses).toContain('running') expect(reducedStatuses).toContain('interrupted') @@ -470,7 +470,7 @@ describe('Issue #129: typed errors do not mask final state', () => { console.log = originalLog }) - class GenerationFailed extends Schema.TaggedError()('GenerationFailed', { + class GenerationFailed extends Schema.TaggedErrorClass()('GenerationFailed', { message: Schema.String, failedCount: Schema.Number, }) {} @@ -486,21 +486,18 @@ describe('Issue #129: typed errors do not mask final state', () => { }), ).pipe( Effect.provide(testModeLayer('json')), - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.sync(() => { // Flat contract: stdout gets the final raw state, no envelope. // Exit code signals failure; the error details live in `cause` and // are surfaced via `formatError` → stderr. expect(capturedOutput).toHaveLength(1) - const state = decodeJson( - Schema.Record({ key: Schema.String, value: Schema.Unknown }), - capturedOutput[0]!, - ) + const state = decodeJson(Schema.Record(Schema.String, Schema.Unknown), capturedOutput[0]!) expect(state.count).toBe(42) expect(state._tag).toBeUndefined() // Typed error still propagates via the Effect channel. - const failures = [...Cause.failures(cause)] + const failures = cause.reasons.filter(Cause.isFailReason).map((reason) => reason.error) expect(failures.some((e) => e instanceof GenerationFailed)).toBe(true) }), ), @@ -520,7 +517,7 @@ describe('Issue #129: typed errors do not mask final state', () => { }), ).pipe( Effect.provide(testModeLayer('ndjson')), - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.sync(() => { // Every state change before the error is on stdout as raw JSON. // No trailing Failure envelope — exit code + stderr carry error info. @@ -529,7 +526,7 @@ describe('Issue #129: typed errors do not mask final state', () => { expect(states.every((s) => s._tag !== 'Failure')).toBe(true) // Typed error still propagates via the Effect channel. - const failures = [...Cause.failures(cause)] + const failures = cause.reasons.filter(Cause.isFailReason).map((reason) => reason.error) expect(failures.some((e) => e instanceof GenerationFailed)).toBe(true) }), ), @@ -557,7 +554,7 @@ describe('run (standalone dual API)', () => { console.log = originalLog }) - class TestError extends Schema.TaggedError()('TestError', { + class TestError extends Schema.TaggedErrorClass()('TestError', { message: Schema.String, }) {} @@ -601,7 +598,7 @@ describe('run (standalone dual API)', () => { it.effect('typed errors are propagated via Effect channel', () => run(CounterApp, () => new TestError({ message: 'test error' })).pipe( Effect.provide(testModeLayer('log')), - Effect.catchAll((error) => + Effect.catch((error) => Effect.sync(() => { expect(error).toBeInstanceOf(TestError) expect(error.message).toBe('test error') @@ -626,7 +623,7 @@ describe('run (standalone dual API)', () => { }), ).pipe( Effect.provide(testModeLayer('json')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput).toHaveLength(1) const state = JSON.parse(capturedOutput[0]!) expect(state).toEqual({ count: 77 }) @@ -697,7 +694,7 @@ describe('runResult', () => { { result: Schema.String }, ).pipe( Effect.provide(testModeLayer('json')), - Effect.andThen((result) => { + Effect.map((result) => { expect(result).toBe('my-secret-value') const stdout = capturedStdout.join('') expect(stdout).toBe('my-secret-value\n') @@ -717,7 +714,7 @@ describe('runResult', () => { { result: Schema.String }, ).pipe( Effect.provide(testModeLayer('json')), - Effect.andThen(() => { + Effect.map(() => { const allOutput = capturedStdout.join('') + capturedConsole.join('') expect(allOutput).not.toContain('Success') expect(allOutput).not.toContain('Failure') @@ -737,7 +734,7 @@ describe('runResult', () => { { result: Schema.String, view: }, ).pipe( Effect.provide(testModeLayer('log')), - Effect.andThen((result) => { + Effect.map((result) => { expect(result).toBe('the-secret') // stdout is byte-for-byte the result (plus a trailing newline). const stdout = capturedStdout.join('') @@ -762,7 +759,7 @@ describe('runResult', () => { { result: Schema.String, view: }, ).pipe( Effect.provide(testModeLayer('tty')), - Effect.andThen((result) => { + Effect.map((result) => { expect(result).toBe('tty-secret') const stdout = capturedStdout.join('') expect(stdout).toBe('tty-secret\n') @@ -789,7 +786,7 @@ describe('runResult', () => { { result: ResultSchema }, ).pipe( Effect.provide(testModeLayer('json')), - Effect.andThen((result) => { + Effect.map((result) => { expect(result).toEqual({ items: ['a', 'b', 'c'], total: 3 }) // `runResult` writes structured results directly via process.stdout // (not Effect.Console) so handler-emitted logs can be routed to @@ -817,7 +814,7 @@ describe('runResult', () => { { result: Schema.String, view: }, ).pipe( Effect.provide(testModeLayer('log')), - Effect.andThen((result) => { + Effect.map((result) => { expect(result).toBe('the-clean-payload') // stdout must be byte-clean: only the result + trailing newline. const stdout = capturedStdout.join('') @@ -836,7 +833,7 @@ describe('runResult', () => { () => runResult(CounterApp, () => Effect.succeed('value'), { result: Schema.String }).pipe( Effect.provide(testModeLayer('ndjson')), - Effect.catchAllDefect((defect) => + Effect.catchDefect((defect) => Effect.sync(() => { expect(defect).toBeInstanceOf(Error) expect((defect as Error).message).toContain('runResult does not support ndjson') @@ -847,7 +844,7 @@ describe('runResult', () => { }) describe('error handling', () => { - class ReadError extends Schema.TaggedError()('ReadError', { + class ReadError extends Schema.TaggedErrorClass()('ReadError', { message: Schema.String, }) {} @@ -856,7 +853,7 @@ describe('runResult', () => { result: Schema.String, }).pipe( Effect.provide(testModeLayer('json')), - Effect.catchAll((error) => + Effect.catch((error) => Effect.sync(() => { expect(error).toBeInstanceOf(ReadError) expect(error.message).toBe('access denied') diff --git a/packages/@overeng/tui-react/test/unit/exit-rendering.test.tsx b/packages/@overeng/tui-react/test/unit/exit-rendering.test.tsx index 58589b7f08..35c3e9201e 100644 --- a/packages/@overeng/tui-react/test/unit/exit-rendering.test.tsx +++ b/packages/@overeng/tui-react/test/unit/exit-rendering.test.tsx @@ -10,7 +10,7 @@ */ import { it } from '@effect/vitest' -import { Effect, Schema } from 'effect' +import { Effect, Fiber, Schema } from 'effect' import React from 'react' import { describe, test, expect, beforeEach, afterEach } from 'vitest' @@ -30,17 +30,17 @@ const TestState = Schema.Struct({ type TestState = Schema.Schema.Type // Action schema WITH Interrupted variant -const TestActionWithInterrupt = Schema.Union( +const TestActionWithInterrupt = Schema.Union([ Schema.TaggedStruct('SetValue', { value: Schema.String }), Schema.TaggedStruct('Interrupted', {}), -) +]) type TestActionWithInterrupt = Schema.Schema.Type // Action schema WITHOUT Interrupted variant -const TestActionNoInterrupt = Schema.Union( +const TestActionNoInterrupt = Schema.Union([ Schema.TaggedStruct('SetValue', { value: Schema.String }), -) +]) type TestActionNoInterrupt = Schema.Schema.Type @@ -91,7 +91,7 @@ const AppNoInterrupt = createTuiApp({ }) const WireState = Schema.TaggedStruct('WireState', { - phase: Schema.Literal('idle', 'done'), + phase: Schema.Literals(['idle', 'done']), text: Schema.String, nullable: Schema.NullOr(Schema.String), note: Schema.optional(Schema.String), @@ -105,18 +105,18 @@ const WireState = Schema.TaggedStruct('WireState', { type WireState = typeof WireState.Type -const WireAction = Schema.Union(Schema.TaggedStruct('SetWireState', { next: WireState })) +const WireAction = Schema.Union([Schema.TaggedStruct('SetWireState', { next: WireState })]) type WireAction = typeof WireAction.Type const WireEvent = Schema.TaggedStruct('WireEvent', { - from: Schema.Literal('idle', 'done'), - to: Schema.Literal('idle', 'done'), + from: Schema.Literals(['idle', 'done']), + to: Schema.Literals(['idle', 'done']), text: Schema.String, }) -const FailurePartitionWire = Schema.parseJson( +const FailurePartitionWire = Schema.fromJsonString( Schema.Struct({ - _tag: Schema.Literal('Failure', 'Success'), + _tag: Schema.Literals(['Failure', 'Success']), cause: Schema.NullOr(Schema.String), }), ) @@ -330,7 +330,7 @@ describe('Interrupt Handling', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('log')), - Effect.andThen(() => { + Effect.map(() => { // After scope closes, Interrupted should have been dispatched // The finalizer dispatches Interrupted, so the last state should have interrupted: true // Note: We need to check the final state after the effect completes @@ -352,7 +352,7 @@ describe('Interrupt Handling', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('log')), - Effect.andThen(() => { + Effect.map(() => { // Should not have interrupted state since schema doesn't have Interrupted expect(states.every((s) => s.interrupted === false)).toBe(true) }), @@ -425,7 +425,7 @@ describe('Final state output', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('json')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput).toHaveLength(1) const state = JSON.parse(capturedOutput[0]!) // Flat contract: stdout is raw state, no envelope. @@ -444,8 +444,8 @@ describe('Final state output', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('json')), - Effect.catchAllDefect((defect) => Effect.succeed({ caught: defect })), - Effect.andThen((result) => { + Effect.catchDefect((defect) => Effect.succeed({ caught: defect })), + Effect.map((result) => { expect(result).toHaveProperty('caught') // State captured at time of failure is still emitted (no envelope). expect(capturedOutput).toHaveLength(1) @@ -472,15 +472,15 @@ describe('Final state output', () => { const fiber = yield* Effect.gen(function* () { const tui = yield* InterruptTestApp.run() tui.dispatch({ _tag: 'SetValue', value: 'before interrupt' }) - yield* Effect.yieldNow() + yield* Effect.yieldNow return yield* Effect.never - }).pipe(Effect.scoped, Effect.provide(testModeLayer('json')), Effect.fork) + }).pipe(Effect.scoped, Effect.provide(testModeLayer('json')), Effect.forkChild) yield* Effect.sleep('50 millis') - yield* fiber.interruptAsFork(fiber.id()) - const _ = yield* fiber.await + yield* Fiber.interruptAs(fiber, fiber.id) + const _ = yield* Fiber.await(fiber) }).pipe( - Effect.andThen(() => { + Effect.map(() => { // Interrupt still emits the final state (no envelope). The interrupt // signal itself rides the Effect channel, not stdout. expect(capturedOutput).toHaveLength(1) @@ -517,7 +517,7 @@ describe('tui-react JSON/NDJSON wire baselines (cross-major invariant)', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('json')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput).toMatchInlineSnapshot(` [ "{"_tag":"WireState","phase":"done","text":"Line 1\\r\\nLine 2 世界 NaN 2026-02-31","nullable":null,"note":"","items":[{"id":"α","value":""},{"id":"big","value":"9007199254740991"}]}", @@ -536,7 +536,7 @@ describe('tui-react JSON/NDJSON wire baselines (cross-major invariant)', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('ndjson')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput.join('\n')).toMatchInlineSnapshot(` "{"_tag":"WireState","phase":"idle","text":"","nullable":null,"items":[]} {"_tag":"WireState","phase":"done","text":"Line 1\\r\\nLine 2 世界 NaN 2026-02-31","nullable":null,"note":"","items":[{"id":"α","value":""},{"id":"big","value":"9007199254740991"}]}" @@ -554,7 +554,7 @@ describe('tui-react JSON/NDJSON wire baselines (cross-major invariant)', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('ndjson')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput.join('\n')).toMatchInlineSnapshot(` "{"_tag":"WireState","phase":"idle","text":"","nullable":null,"items":[]} {"_tag":"WireEvent","from":"idle","to":"done","text":"Line 1\\r\\nLine 2 世界 NaN 2026-02-31"}" @@ -575,7 +575,7 @@ describe('tui-react JSON/NDJSON wire baselines (cross-major invariant)', () => { const tui = yield* InvalidApp.run() tui.dispatch({ next: { ...finalWireState, text: null } }) }).pipe(Effect.scoped, Effect.provide(testModeLayer('json')), Effect.exit) - const failureBytes = yield* Schema.encode(FailurePartitionWire)({ + const failureBytes = yield* Schema.encodeEffect(FailurePartitionWire)({ _tag: exit._tag, cause: exit._tag === 'Failure' && exit.cause !== undefined @@ -583,10 +583,10 @@ describe('tui-react JSON/NDJSON wire baselines (cross-major invariant)', () => { : null, }) - // TODO(live-migration:effect-3-4): Effect 4 may print help on stdout here (effect#6313); apply #980 rather than accepting bytes on the machine stream. + // TODO(live-migration:effect-3-4): Effect 4's formatter-derived SchemaError prose differs from the v3 ParseError tree; tracked by #978. Both majors produce a defect here because the encode failure passes through Effect.orDie. expect(capturedOutput).toMatchInlineSnapshot(`[]`) expect(failureBytes).toMatchInlineSnapshot( - `"{"_tag":"Failure","cause":"ParseError: (parseJson <-> { readonly _tag: \\"WireState\\"; readonly phase: \\"idle\\" | \\"done\\"; readonly text: string; readonly nullable: string | null; readonly note?: string | undefined; readonly items: ReadonlyArray<{ readonly id: string; readonly value: string }> })\\n└─ Type side transformation failure\\n └─ { readonly _tag: \\"WireState\\"; readonly phase: \\"idle\\" | \\"done\\"; readonly text: string; readonly nullable: string | null; readonly note?: string | undefined; readonly items: ReadonlyArray<{ readonly id: string; readonly value: string }> }\\n └─ [\\"text\\"]\\n └─ Expected string, actual null"}"`, + `"{"_tag":"Failure","cause":"Cause([Die(SchemaError(Expected string, got null\\n at [\\"text\\"]))])"}"`, ) }), ) diff --git a/packages/@overeng/tui-react/test/unit/terminal-input.test.ts b/packages/@overeng/tui-react/test/unit/terminal-input.test.ts index 8c0d5b07f2..7f4d951264 100644 --- a/packages/@overeng/tui-react/test/unit/terminal-input.test.ts +++ b/packages/@overeng/tui-react/test/unit/terminal-input.test.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'node:events' import type { Readable } from 'node:stream' -import { Cause, Effect, Exit, Stream } from 'effect' +import { Cause, Effect, Exit, Fiber, Stream } from 'effect' import { afterEach, describe, expect, it, vi } from 'vitest' import { createTerminalInput } from '../../src/effect/TerminalInput.ts' @@ -47,16 +47,16 @@ describe('createTerminalInput', () => { return Effect.void }), - Effect.fork, + Effect.forkChild, ) - yield* Effect.yieldNow() + yield* Effect.yieldNow input.emitData(Buffer.from([0x03])) - const fiberExit = yield* fiber.await + const fiberExit = yield* Fiber.await(fiber) expect(Exit.isFailure(fiberExit)).toBe(true) if (Exit.isFailure(fiberExit) === true) { - expect(Cause.isInterruptedOnly(fiberExit.cause)).toBe(true) + expect(Cause.hasInterruptsOnly(fiberExit.cause)).toBe(true) } }), ), diff --git a/packages/@overeng/tui-react/test/unit/useTuiState.test.tsx b/packages/@overeng/tui-react/test/unit/useTuiState.test.tsx index 8e0bc74290..0418cd6c55 100644 --- a/packages/@overeng/tui-react/test/unit/useTuiState.test.tsx +++ b/packages/@overeng/tui-react/test/unit/useTuiState.test.tsx @@ -13,7 +13,7 @@ import { createTuiApp } from '../../src/effect/TuiApp.tsx' // Test State and Action Schemas // ============================================================================= -const TestState = Schema.Union( +const TestState = Schema.Union([ Schema.TaggedStruct('Idle', {}), Schema.TaggedStruct('Running', { count: Schema.Number, @@ -21,15 +21,15 @@ const TestState = Schema.Union( Schema.TaggedStruct('Complete', { total: Schema.Number, }), -) +]) type TestState = Schema.Schema.Type -const TestAction = Schema.Union( +const TestAction = Schema.Union([ Schema.TaggedStruct('Start', {}), Schema.TaggedStruct('Increment', {}), Schema.TaggedStruct('Finish', { total: Schema.Number }), -) +]) type TestAction = Schema.Schema.Type @@ -134,7 +134,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('json')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput).toHaveLength(1) const parsed = JSON.parse(capturedOutput[0]!) expect(parsed).toEqual({ _tag: 'Complete', total: 10 }) @@ -146,7 +146,7 @@ describe('createTuiApp', () => { TestApp.run().pipe( Effect.scoped, Effect.provide(testModeLayer('json')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput).toHaveLength(1) const parsed = JSON.parse(capturedOutput[0]!) expect(parsed).toEqual({ _tag: 'Idle' }) @@ -167,7 +167,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('ndjson')), - Effect.andThen(() => { + Effect.map(() => { // Initial snapshot + one line per state change. No trailing envelope. expect(capturedOutput.length).toBeGreaterThanOrEqual(2) @@ -194,7 +194,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('log')), - Effect.andThen(() => { + Effect.map(() => { expect(capturedOutput).toHaveLength(0) }), ), @@ -218,7 +218,7 @@ describe('createTuiApp', () => { }).pipe( Effect.scoped, Effect.provide(testModeLayer('log')), - Effect.andThen(() => { + Effect.map(() => { expect(states).toEqual([{ _tag: 'Idle' }, { _tag: 'Running', count: 0 }]) }), ) diff --git a/packages/@overeng/tui-stories/bin/tui-stories.tsx b/packages/@overeng/tui-stories/bin/tui-stories.tsx index f2d26def77..d17d5e5820 100644 --- a/packages/@overeng/tui-stories/bin/tui-stories.tsx +++ b/packages/@overeng/tui-stories/bin/tui-stories.tsx @@ -1,7 +1,7 @@ #!/usr/bin/env bun -import { Command } from '@effect/cli' -import { NodeContext, NodeRuntime } from '@effect/platform-node' +import { Command } from 'effect/unstable/cli' +import { NodeServices, NodeRuntime } from '@effect/platform-node' import { Effect } from 'effect' import { runTuiMain } from '@overeng/tui-react/node' @@ -26,6 +26,6 @@ cli(rewriteHelpSubcommand(process.argv)).pipe( Effect.scoped, CliVersion.enrichErrors, Effect.provideService(CliVersion, { name: 'tui-stories', version }), - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), runTuiMain(NodeRuntime), ) diff --git a/packages/@overeng/tui-stories/nix/build.nix b/packages/@overeng/tui-stories/nix/build.nix index bbb39288ae..9bcd070ea7 100644 --- a/packages/@overeng/tui-stories/nix/build.nix +++ b/packages/@overeng/tui-stories/nix/build.nix @@ -21,7 +21,7 @@ let workspaceRoot = src; # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { - "." = mkSharedHash "sha256-LK4w3JOeRzBwcVNzCpcMFv7nv2dEOX6yPVnvGJNXxG8="; + "." = mkSharedHash "sha256-d2o45DnHYQEb57De9MdwYkVCA4cRgOWcH0AIdkmN1Cs="; }; nativeNodePackages = opentuiCoreNative.packages; inherit gitRev commitTs dirty; diff --git a/packages/@overeng/tui-stories/package.json b/packages/@overeng/tui-stories/package.json index 35769b2ccb..f012ab9a43 100644 --- a/packages/@overeng/tui-stories/package.json +++ b/packages/@overeng/tui-stories/package.json @@ -17,19 +17,10 @@ "storybook:build": "storybook build" }, "dependencies": { - "@effect-atom/atom": "0.5.3", - "@effect-atom/atom-react": "0.5.0", - "@effect/cli": "0.75.2", - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/printer": "0.49.0", - "@effect/printer-ansi": "0.49.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/atom-react": "4.0.0-beta.102", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@opentui/core": "0.4.1", "@opentui/react": "0.4.1", "@overeng/tui-core": "workspace:^", @@ -39,21 +30,21 @@ "@storybook/react": "10.4.6", "@types/react": "19.2.17", "@types/react-reconciler": "0.33.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "react": "19.2.7", "react-dom": "19.2.7", "react-reconciler": "0.33.0", "vitest": "4.1.9" }, "devDependencies": { - "@effect/cli": "0.75.2", - "@effect/vitest": "0.29.0", + "@effect/vitest": "4.0.0-beta.102", "@overeng/megarepo": "workspace:^", "@overeng/utils-dev": "workspace:^", "@storybook/react": "10.4.6", "@storybook/react-vite": "10.4.6", "@types/node": "26.0.0", "@types/react": "19.2.17", + "effect": "4.0.0-beta.102", "storybook": "10.4.6", "typescript": "6.0.3", "vitest": "4.1.9" @@ -64,23 +55,16 @@ } }, "peerDependencies": { - "@effect-atom/atom": "^0.5.3", - "@effect-atom/atom-react": "^0.5.0", - "@effect/cli": "^0.75.2", - "@effect/cluster": "^0.59.0", - "@effect/experimental": "^0.60.0", - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/rpc": "^0.75.1", - "@effect/workflow": "^0.18.2", + "@effect/atom-react": "^4.0.0-beta.102", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@opentui/core": "^0.4.1", "@opentui/react": "^0.4.1", "@playwright/test": "^1.61.0", "@storybook/react": "^10.4.6", "@types/react": "^19.2.17", "@types/react-reconciler": "^0.33.0", - "effect": "^3.21.4", + "effect": "^4.0.0-beta.102", "react": "^19.2.7", "react-dom": "^19.2.7", "react-reconciler": "^0.33.0" diff --git a/packages/@overeng/tui-stories/package.json.genie.ts b/packages/@overeng/tui-stories/package.json.genie.ts index 09cd7716ac..97f43277ed 100644 --- a/packages/@overeng/tui-stories/package.json.genie.ts +++ b/packages/@overeng/tui-stories/package.json.genie.ts @@ -12,7 +12,7 @@ import tuiReactPkg from '../tui-react/package.json.genie.ts' import utilsDevPkg from '../utils-dev/package.json.genie.ts' import utilsPkg from '../utils/package.json.genie.ts' -const ownPeerDepNames = ['@effect/cli'] as const +const ownPeerDepNames = ['effect'] as const const runtimeDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/tui-stories' }), @@ -22,17 +22,7 @@ const runtimeDeps = catalog.compose({ devDependencies: { workspace: [megarepoPkg, utilsDevPkg], external: { - ...catalog.pick( - ...ownPeerDepNames, - '@effect/vitest', - '@storybook/react', - '@storybook/react-vite', - '@types/node', - '@types/react', - 'storybook', - 'typescript', - 'vitest', - ), + ...catalog.pick(...ownPeerDepNames, '@effect/vitest', '@storybook/react', '@storybook/react-vite', '@types/node', '@types/react', 'storybook', 'typescript', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/tui-stories/src/StoryCapture.ts b/packages/@overeng/tui-stories/src/StoryCapture.ts index fa000121ed..71444b007f 100644 --- a/packages/@overeng/tui-stories/src/StoryCapture.ts +++ b/packages/@overeng/tui-stories/src/StoryCapture.ts @@ -8,7 +8,7 @@ * TuiStoryPreview element props before it mounts. */ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import type { Schema } from 'effect' import React, { type ReactElement, type ComponentType } from 'react' diff --git a/packages/@overeng/tui-stories/src/StoryDiscovery.ts b/packages/@overeng/tui-stories/src/StoryDiscovery.ts index 8b629f6120..b091ad1f44 100644 --- a/packages/@overeng/tui-stories/src/StoryDiscovery.ts +++ b/packages/@overeng/tui-stories/src/StoryDiscovery.ts @@ -106,7 +106,7 @@ export const discoverStories = (options: { // ============================================================================= /** Effect service for story discovery (enables dependency injection in tests) */ -export class StoryDiscovery extends Context.Tag('StoryDiscovery')< +export class StoryDiscovery extends Context.Service< StoryDiscovery, { readonly discover: (options: { @@ -114,7 +114,7 @@ export class StoryDiscovery extends Context.Tag('StoryDiscovery')< readonly patterns?: readonly string[] }) => Effect.Effect } ->() { +>()('StoryDiscovery') { static readonly live = StoryDiscovery.of({ discover: discoverStories, }) diff --git a/packages/@overeng/tui-stories/src/StoryRenderer.ts b/packages/@overeng/tui-stories/src/StoryRenderer.ts index 47b5bec857..2af2e4e63f 100644 --- a/packages/@overeng/tui-stories/src/StoryRenderer.ts +++ b/packages/@overeng/tui-stories/src/StoryRenderer.ts @@ -7,8 +7,8 @@ * - ndjson: Timeline events as newline-delimited JSON */ -import { Atom, Registry } from '@effect-atom/atom' import { Effect, Schema } from 'effect' +import { Atom, AtomRegistry } from 'effect/unstable/reactivity' import React from 'react' import { @@ -159,7 +159,7 @@ const renderReact = ({ }): Effect.Effect => Effect.gen(function* () { const targetState = computeState({ captured, timelineMode }) - const registry = Registry.make() + const registry = AtomRegistry.make() const stateAtom = Atom.make(targetState) const viewElement = React.createElement(captured.View, { stateAtom }) @@ -185,9 +185,9 @@ const renderReact = ({ // ============================================================================= /** Encode an arbitrary (already schema-encoded) value to a JSON string. */ -const encodeJson = Schema.encodeSync(Schema.parseJson(Schema.Unknown)) +const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)) /** Same, but pretty-printed with a 2-space indent (json output mode). */ -const encodeJsonPretty = Schema.encodeSync(Schema.parseJson(Schema.Unknown, { space: 2 })) +const encodeJsonPretty = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown, { space: 2 })) /** Encode state as JSON via the app's stateSchema */ const renderJson = ({ diff --git a/packages/@overeng/tui-stories/src/cli/inspect.ts b/packages/@overeng/tui-stories/src/cli/inspect.ts index 86057986ab..4cc4c628fd 100644 --- a/packages/@overeng/tui-stories/src/cli/inspect.ts +++ b/packages/@overeng/tui-stories/src/cli/inspect.ts @@ -1,5 +1,5 @@ -import { Args, Command, Options } from '@effect/cli' import { Effect } from 'effect' +import { Argument as Args, Command, Flag as Options } from 'effect/unstable/cli' import React from 'react' import { run } from '@overeng/tui-react' @@ -10,11 +10,11 @@ import { discoverStories } from '../StoryDiscovery.ts' import { findStory } from '../StoryModule.ts' import { InspectApp, InspectView } from './renderers/InspectOutput/mod.ts' -const storyIdArg = Args.text({ name: 'story-id' }).pipe( +const storyIdArg = Args.string('story-id').pipe( Args.withDescription('Story title or ID to inspect'), ) -const pathOption = Options.text('path').pipe( +const pathOption = Options.string('path').pipe( Options.withDescription('Package directory to search for stories'), ) diff --git a/packages/@overeng/tui-stories/src/cli/list.ts b/packages/@overeng/tui-stories/src/cli/list.ts index 10709985f2..6f21b7bfb6 100644 --- a/packages/@overeng/tui-stories/src/cli/list.ts +++ b/packages/@overeng/tui-stories/src/cli/list.ts @@ -1,5 +1,5 @@ -import { Command, Options } from '@effect/cli' import { Effect } from 'effect' +import { Command, Flag as Options } from 'effect/unstable/cli' import React from 'react' import { run } from '@overeng/tui-react' @@ -8,7 +8,7 @@ import { outputOption, outputModeLayer } from '@overeng/tui-react/node' import { discoverStories } from '../StoryDiscovery.ts' import { ListApp, ListView } from './renderers/ListOutput/mod.ts' -const pathOption = Options.text('path').pipe( +const pathOption = Options.string('path').pipe( Options.withDescription('Package directory to search for stories'), ) diff --git a/packages/@overeng/tui-stories/src/cli/mod.ts b/packages/@overeng/tui-stories/src/cli/mod.ts index f2760161db..580dd7fc84 100644 --- a/packages/@overeng/tui-stories/src/cli/mod.ts +++ b/packages/@overeng/tui-stories/src/cli/mod.ts @@ -1,4 +1,4 @@ -import { Command } from '@effect/cli' +import { Command } from 'effect/unstable/cli' import { inspectCommand } from './inspect.ts' import { listCommand } from './list.ts' diff --git a/packages/@overeng/tui-stories/src/cli/render.ts b/packages/@overeng/tui-stories/src/cli/render.ts index 65c460761a..f29ae93976 100644 --- a/packages/@overeng/tui-stories/src/cli/render.ts +++ b/packages/@overeng/tui-stories/src/cli/render.ts @@ -1,5 +1,5 @@ -import { Args, Command, Options } from '@effect/cli' import { Effect } from 'effect' +import { Argument as Args, Command, Flag as Options } from 'effect/unstable/cli' import React from 'react' import { run, isJson } from '@overeng/tui-react' @@ -11,17 +11,17 @@ import { findStory, parseArgOverrides } from '../StoryModule.ts' import { renderStory, type TimelineMode } from '../StoryRenderer.ts' import { RenderApp, RenderView } from './renderers/RenderOutput/mod.ts' -const storyIdArg = Args.text({ name: 'story-id' }).pipe( +const storyIdArg = Args.string('story-id').pipe( Args.withDescription('Story title or ID to render (supports prefix/substring match)'), ) -const storyNameOption = Options.text('story').pipe( +const storyNameOption = Options.string('story').pipe( Options.withAlias('s'), Options.withDescription('Named story export to render (defaults to first)'), Options.optional, ) -const pathOption = Options.text('path').pipe( +const pathOption = Options.string('path').pipe( Options.withDescription('Package directory to search for stories'), ) @@ -41,7 +41,7 @@ const atOption = Options.integer('at').pipe( Options.optional, ) -const argOption = Options.text('arg').pipe( +const argOption = Options.string('arg').pipe( Options.withAlias('a'), Options.withDescription('Override story arg (key=value format, repeatable)'), Options.repeated, diff --git a/packages/@overeng/tui-stories/src/cli/renderers/InspectOutput/schema.ts b/packages/@overeng/tui-stories/src/cli/renderers/InspectOutput/schema.ts index 5d1b7a469f..17dc942bd6 100644 --- a/packages/@overeng/tui-stories/src/cli/renderers/InspectOutput/schema.ts +++ b/packages/@overeng/tui-stories/src/cli/renderers/InspectOutput/schema.ts @@ -25,7 +25,9 @@ export const InspectState = Schema.Struct({ export type InspectStateType = typeof InspectState.Type /** Actions dispatched to update inspect output state */ -export const InspectAction = Schema.Union(Schema.TaggedStruct('SetState', { state: InspectState })) +export const InspectAction = Schema.Union([ + Schema.TaggedStruct('SetState', { state: InspectState }), +]) export type InspectActionType = typeof InspectAction.Type diff --git a/packages/@overeng/tui-stories/src/cli/renderers/InspectOutput/view.tsx b/packages/@overeng/tui-stories/src/cli/renderers/InspectOutput/view.tsx index 493d165e35..00882cf02b 100644 --- a/packages/@overeng/tui-stories/src/cli/renderers/InspectOutput/view.tsx +++ b/packages/@overeng/tui-stories/src/cli/renderers/InspectOutput/view.tsx @@ -1,4 +1,4 @@ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue } from '@overeng/tui-react' diff --git a/packages/@overeng/tui-stories/src/cli/renderers/ListOutput/schema.ts b/packages/@overeng/tui-stories/src/cli/renderers/ListOutput/schema.ts index 724426fbeb..5d3f21203d 100644 --- a/packages/@overeng/tui-stories/src/cli/renderers/ListOutput/schema.ts +++ b/packages/@overeng/tui-stories/src/cli/renderers/ListOutput/schema.ts @@ -23,7 +23,7 @@ export const ListState = Schema.Struct({ export type ListStateType = typeof ListState.Type /** Actions dispatched to update list output state */ -export const ListAction = Schema.Union(Schema.TaggedStruct('SetState', { state: ListState })) +export const ListAction = Schema.Union([Schema.TaggedStruct('SetState', { state: ListState })]) export type ListActionType = typeof ListAction.Type diff --git a/packages/@overeng/tui-stories/src/cli/renderers/ListOutput/view.tsx b/packages/@overeng/tui-stories/src/cli/renderers/ListOutput/view.tsx index 40ce9ef403..6b8010383d 100644 --- a/packages/@overeng/tui-stories/src/cli/renderers/ListOutput/view.tsx +++ b/packages/@overeng/tui-stories/src/cli/renderers/ListOutput/view.tsx @@ -1,4 +1,4 @@ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, useTuiAtomValue } from '@overeng/tui-react' diff --git a/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/schema.ts b/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/schema.ts index 9147b6293a..0f71d40e2a 100644 --- a/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/schema.ts +++ b/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/schema.ts @@ -8,7 +8,7 @@ import { Schema } from 'effect' * Note: the command's own output mode (--output) is handled by outputModeLayer, * not tracked in this state. This state models the *content* being rendered. */ -export const RenderState = Schema.Union( +export const RenderState = Schema.Union([ Schema.TaggedStruct('Rendering', { storyId: Schema.String, width: Schema.Number, @@ -24,12 +24,12 @@ export const RenderState = Schema.Union( storyId: Schema.String, message: Schema.String, }), -) +]) export type RenderStateType = typeof RenderState.Type /** Actions for the render output */ -export const RenderAction = Schema.Union(Schema.TaggedStruct('SetState', { state: RenderState })) +export const RenderAction = Schema.Union([Schema.TaggedStruct('SetState', { state: RenderState })]) export type RenderActionType = typeof RenderAction.Type diff --git a/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/stories/_megarepo-renders.ts b/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/stories/_megarepo-renders.ts index 5e8f3a6f7d..b19977603d 100644 --- a/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/stories/_megarepo-renders.ts +++ b/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/stories/_megarepo-renders.ts @@ -10,7 +10,7 @@ * Vite resolves to the megarepo source tree at build time. */ -import { Atom } from '@effect-atom/atom' +import { Atom } from 'effect/unstable/reactivity' import { ExecView } from '@megarepo-internal/cli/renderers/ExecOutput/mod.ts' import * as execFixtures from '@megarepo-internal/cli/renderers/ExecOutput/stories/_fixtures.ts' import { StatusView } from '@megarepo-internal/cli/renderers/StatusOutput/mod.ts' diff --git a/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/stories/_render-helper.ts b/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/stories/_render-helper.ts index e7a7b231b4..bff20c562a 100644 --- a/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/stories/_render-helper.ts +++ b/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/stories/_render-helper.ts @@ -6,8 +6,8 @@ * actual CLI output. */ -import type { Atom } from '@effect-atom/atom' -import { Registry } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' +import { AtomRegistry } from 'effect/unstable/reactivity' import React from 'react' import { @@ -30,7 +30,7 @@ export const renderViewToLines = async ({ readonly width?: number readonly renderConfig?: RenderConfig }): Promise => { - const registry = Registry.make() + const registry = AtomRegistry.make() const viewElement = React.createElement(View, { stateAtom }) const configElement = React.createElement( diff --git a/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/view.tsx b/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/view.tsx index 9a2bd376bc..8e4ef5a367 100644 --- a/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/view.tsx +++ b/packages/@overeng/tui-stories/src/cli/renderers/RenderOutput/view.tsx @@ -1,4 +1,4 @@ -import type { Atom } from '@effect-atom/atom' +import type { Atom } from 'effect/unstable/reactivity' import React from 'react' import { Box, Text, Spinner, useTuiAtomValue } from '@overeng/tui-react' diff --git a/packages/@overeng/tui-stories/test/StoryCapture.test.ts b/packages/@overeng/tui-stories/test/StoryCapture.test.ts index 25c14669f5..7fef875569 100644 --- a/packages/@overeng/tui-stories/test/StoryCapture.test.ts +++ b/packages/@overeng/tui-stories/test/StoryCapture.test.ts @@ -13,7 +13,7 @@ const MEGAREPO_DIR = resolve(WORKSPACE_ROOT, 'packages/@overeng/megarepo') /* Story discovery is slow on CI (glob + sequential imports due to Bun TDZ workaround can take >5s). Provide it as a layer so it runs once in beforeAll — independent of per-test timeouts — and is shared across all tests via dependency injection. */ -class TestStories extends Context.Tag('TestStories')() { +class TestStories extends Context.Service()('TestStories') { static readonly layer = Layer.effect( TestStories, discoverStories({ packageDirs: [MEGAREPO_DIR] }), diff --git a/packages/@overeng/tui-stories/test/StoryRenderer.test.ts b/packages/@overeng/tui-stories/test/StoryRenderer.test.ts index 742121832f..24f0a1a9a2 100644 --- a/packages/@overeng/tui-stories/test/StoryRenderer.test.ts +++ b/packages/@overeng/tui-stories/test/StoryRenderer.test.ts @@ -9,7 +9,7 @@ import { findStory } from '../src/StoryModule.ts' import { renderStory } from '../src/StoryRenderer.ts' /** Permissive JSON decode for asserting render output shape. */ -const parseJson = Schema.decodeSync(Schema.parseJson(Schema.Unknown)) +const parseJson = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown)) const WORKSPACE_ROOT = resolve(import.meta.dirname, '../../../..') const MEGAREPO_DIR = resolve(WORKSPACE_ROOT, 'packages/@overeng/megarepo') @@ -17,7 +17,7 @@ const MEGAREPO_DIR = resolve(WORKSPACE_ROOT, 'packages/@overeng/megarepo') /* Story discovery is slow on CI (glob + sequential imports due to Bun TDZ workaround can take >5s). Provide it as a layer so it runs once in beforeAll — independent of per-test timeouts — and is shared across all tests via dependency injection. */ -class TestStories extends Context.Tag('TestStories')() { +class TestStories extends Context.Service()('TestStories') { static readonly layer = Layer.effect( TestStories, discoverStories({ packageDirs: [MEGAREPO_DIR] }), diff --git a/packages/@overeng/utils-dev/package.json b/packages/@overeng/utils-dev/package.json index f02c9cad51..d98dc2dee7 100644 --- a/packages/@overeng/utils-dev/package.json +++ b/packages/@overeng/utils-dev/package.json @@ -17,21 +17,19 @@ } }, "devDependencies": { - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/vitest": "0.29.0", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "typescript": "6.0.3", "vitest": "4.1.9" }, "peerDependencies": { - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/vitest": "^0.29.0", - "effect": "^3.21.4", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", + "@effect/vitest": "^4.0.0-beta.102", + "effect": "^4.0.0-beta.102", "vitest": "^4.1.9" }, "$genie": { diff --git a/packages/@overeng/utils-dev/package.json.genie.ts b/packages/@overeng/utils-dev/package.json.genie.ts index 309326a9dc..c22d27cef8 100644 --- a/packages/@overeng/utils-dev/package.json.genie.ts +++ b/packages/@overeng/utils-dev/package.json.genie.ts @@ -8,14 +8,7 @@ import { } from '../../../genie/internal.ts' /** Packages exposed as peer deps (consumers provide) + included in devDeps (for local dev/test) */ -const peerDepNames = [ - '@effect/opentelemetry', - '@effect/platform', - '@effect/platform-node', - '@effect/vitest', - 'effect', - 'vitest', -] as const +const peerDepNames = ['@effect/opentelemetry', 'effect', '@effect/platform-node', '@effect/vitest', 'vitest'] as const const deps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/utils-dev' }), diff --git a/packages/@overeng/utils-dev/src/node-vitest/Vitest.ts b/packages/@overeng/utils-dev/src/node-vitest/Vitest.ts index c838d8f452..07e63b68a4 100644 --- a/packages/@overeng/utils-dev/src/node-vitest/Vitest.ts +++ b/packages/@overeng/utils-dev/src/node-vitest/Vitest.ts @@ -12,7 +12,6 @@ import * as inspector from 'node:inspector' import { OtlpSerialization, OtlpTracer } from '@effect/opentelemetry' -import { FetchHttpClient } from '@effect/platform' import type * as Vitest from '@effect/vitest' import type { Duration } from 'effect' import { @@ -25,6 +24,7 @@ import { type Schema, type Scope, } from 'effect' +import { FetchHttpClient } from 'effect/unstable/http' import { otlpTracesUrl } from '../otelite/otlp-url.ts' @@ -132,7 +132,7 @@ export type WithTestCtxParams = { * const withTestCtx = makeWithTestCtx({ * timeout: Duration.minutes(2), * makeLayer: (testContext) => Layer.mergeAll( - * NodeContext.layer, + * NodeServices.layer, * MyService.layer, * ), * }) @@ -290,7 +290,7 @@ const normalizePropOptions = ( * * @example * ```typescript - * const StorageType = Schema.Literal('memory', 'fs') + * const StorageType = Schema.Literals(['memory', 'fs']) * const Count = Schema.Int.pipe(Schema.between(1, 100)) * * Vitest.asProp( diff --git a/packages/@overeng/utils-dev/src/node-vitest/otel-vitest-flush.test.ts b/packages/@overeng/utils-dev/src/node-vitest/otel-vitest-flush.test.ts index 9ed1c771ab..0f925f1f12 100644 --- a/packages/@overeng/utils-dev/src/node-vitest/otel-vitest-flush.test.ts +++ b/packages/@overeng/utils-dev/src/node-vitest/otel-vitest-flush.test.ts @@ -9,7 +9,7 @@ * layer-scoped program never silently drops its last span batch. */ -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer, Scope } from 'effect' import { describe, expect, it } from 'vitest' @@ -54,7 +54,7 @@ describe('makeOtelVitestLayer — flush on shutdown', () => { const post = yield* cap.inspect({ signal: 'traces', service: SERVICE, name: SPAN }) return { pre: pre.length, post: post.length } - }).pipe(Effect.scoped, Effect.provide(Layer.provideMerge(Otelite.Default, NodeContext.layer))) + }).pipe(Effect.scoped, Effect.provide(Layer.provideMerge(Otelite.Default, NodeServices.layer))) const { pre, post } = await Effect.runPromise(program) expect(pre).toBe(0) diff --git a/packages/@overeng/utils-dev/src/node-vitest/setup-fast-check.ts b/packages/@overeng/utils-dev/src/node-vitest/setup-fast-check.ts index f6679ab1ec..43f5db852a 100644 --- a/packages/@overeng/utils-dev/src/node-vitest/setup-fast-check.ts +++ b/packages/@overeng/utils-dev/src/node-vitest/setup-fast-check.ts @@ -1,4 +1,4 @@ -import * as FastCheck from 'effect/FastCheck' +import * as FastCheck from 'effect/testing/FastCheck' type FastCheckGlobalParameters = Parameters[0] diff --git a/packages/@overeng/utils-dev/src/otelite/Otelite.test.ts b/packages/@overeng/utils-dev/src/otelite/Otelite.test.ts index 3ff26c2715..5c196173b0 100644 --- a/packages/@overeng/utils-dev/src/otelite/Otelite.test.ts +++ b/packages/@overeng/utils-dev/src/otelite/Otelite.test.ts @@ -1,7 +1,7 @@ import { access } from 'node:fs/promises' -import { Command } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { ChildProcess as Command } from 'effect/unstable/process' +import { NodeServices } from '@effect/platform-node' import { describe, expect, it } from '@effect/vitest' import { Effect, Exit, Layer, Schema } from 'effect' @@ -43,7 +43,7 @@ const captureSpanPayload = (name: string, service: string) => }) /** Real otelite binary (from `PATH`, see README) + Node platform layer. */ -const TestLayer = Otelite.Default.pipe(Layer.provideMerge(NodeContext.layer)) +const TestLayer = Otelite.Default.pipe(Layer.provideMerge(NodeServices.layer)) const tracesFixture = new URL('./fixtures/traces.ndjson', import.meta.url).pathname const emitter = new URL('./emitter.ts', import.meta.url).pathname diff --git a/packages/@overeng/utils-dev/src/otelite/Otelite.ts b/packages/@overeng/utils-dev/src/otelite/Otelite.ts index e08dfd9565..dcbda44b72 100644 --- a/packages/@overeng/utils-dev/src/otelite/Otelite.ts +++ b/packages/@overeng/utils-dev/src/otelite/Otelite.ts @@ -1,4 +1,5 @@ -import { Command, CommandExecutor, FileSystem } from '@effect/platform' +import { ChildProcess as Command, ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' +import { FileSystem } from 'effect/FileSystem' import { Deferred, Effect, Fiber, Ref, Schedule, Schema, type Scope, Stream } from 'effect' import { @@ -123,8 +124,8 @@ type InspectRow = S extends 'traces' ? MetricRow : LogRow -const decodeSummary = Schema.decodeUnknown(Schema.parseJson(Summary)) -const decodeEndpointsEvent = Schema.decodeUnknown(Schema.parseJson(EndpointsEvent)) +const decodeSummary = Schema.decodeUnknown(Schema.fromJsonString(Summary)) +const decodeEndpointsEvent = Schema.decodeUnknown(Schema.fromJsonString(EndpointsEvent)) const rowSchema = { traces: SpanRow, metrics: MetricRow, logs: LogRow } as const const rowKind = { traces: 'span', metrics: 'metric', logs: 'log' } as const @@ -287,14 +288,14 @@ export class Otelite extends Effect.Service()('@overeng/utils-dev/oteli const schema = summarySchema[signal] const kind = summaryKind[signal] return runCli(args, (stdout) => - Schema.decodeUnknown(Schema.parseJson(schema))(stdout).pipe( + Schema.decodeUnknown(Schema.fromJsonString(schema))(stdout).pipe( Effect.mapError((cause) => new OteliteDecodeError({ kind, raw: stdout, cause })), ), ).pipe(withOteliteInspectSummarySpan(signal)) } const schema = rowSchema[signal] const kind = rowKind[signal] - const decodeRow = Schema.decodeUnknown(Schema.parseJson(schema)) + const decodeRow = Schema.decodeUnknown(Schema.fromJsonString(schema)) return runCli(args, (stdout) => Effect.forEach( stdout.split('\n').filter((line) => line.trim() !== ''), diff --git a/packages/@overeng/utils-dev/src/otelite/diagnostics.ts b/packages/@overeng/utils-dev/src/otelite/diagnostics.ts index 70dcb96028..dca1593f66 100644 --- a/packages/@overeng/utils-dev/src/otelite/diagnostics.ts +++ b/packages/@overeng/utils-dev/src/otelite/diagnostics.ts @@ -1,6 +1,6 @@ import * as NodePath from 'node:path' -import { FileSystem } from '@effect/platform' +import { FileSystem } from 'effect/FileSystem' import { Effect, Schema } from 'effect' import type { CaptureHandle } from './Otelite.ts' @@ -11,7 +11,7 @@ export const TraceJson = Schema.Struct({ schema: Schema.Literal('otelite.trace-json/v1'), summary: TraceSummary, spans: Schema.Array(SpanRow), -}).annotations({ identifier: 'Otelite.TraceJson' }) +}).annotate({ identifier: 'Otelite.TraceJson' }) export type TraceJson = typeof TraceJson.Type /** Versioned metrics diagnostic bundle written to `metrics.json`. */ @@ -19,7 +19,7 @@ export const MetricsJson = Schema.Struct({ schema: Schema.Literal('otelite.metrics-json/v1'), summary: MetricSummary, metrics: Schema.Array(MetricRow), -}).annotations({ identifier: 'Otelite.MetricsJson' }) +}).annotate({ identifier: 'Otelite.MetricsJson' }) export type MetricsJson = typeof MetricsJson.Type /** Versioned log diagnostic bundle written to `logs.json`. */ @@ -27,7 +27,7 @@ export const LogsJson = Schema.Struct({ schema: Schema.Literal('otelite.logs-json/v1'), summary: LogSummary, logs: Schema.Array(LogRow), -}).annotations({ identifier: 'Otelite.LogsJson' }) +}).annotate({ identifier: 'Otelite.LogsJson' }) export type LogsJson = typeof LogsJson.Type /** Paths written by `writeCaptureDiagnostics`. */ @@ -38,7 +38,7 @@ export const DiagnosticFiles = Schema.Struct({ metricsSummaryJson: Schema.optional(Schema.String), logsJson: Schema.optional(Schema.String), logsSummaryJson: Schema.optional(Schema.String), -}).annotations({ identifier: 'Otelite.DiagnosticFiles' }) +}).annotate({ identifier: 'Otelite.DiagnosticFiles' }) export type DiagnosticFiles = typeof DiagnosticFiles.Type /** Inputs for writing a diagnostic bundle from an existing otelite capture. */ @@ -161,7 +161,7 @@ export const writeCaptureDiagnostics = Effect.fn('otelite.diagnostics.write', { const encodeJson = (schema: Schema.Schema) => (value: A): string => - Schema.encodeSync(Schema.parseJson(schema, { space: 2 }))(value) + Schema.encodeSync(Schema.fromJsonString(schema, { space: 2 }))(value) const writeJson = ({ path, content }: { readonly path: string; readonly content: string }) => FileSystem.FileSystem.pipe(Effect.flatMap((fs) => fs.writeFileString(path, `${content}\n`))) diff --git a/packages/@overeng/utils-dev/src/otelite/emitter.ts b/packages/@overeng/utils-dev/src/otelite/emitter.ts index 433cca71ba..76e79b9fb1 100644 --- a/packages/@overeng/utils-dev/src/otelite/emitter.ts +++ b/packages/@overeng/utils-dev/src/otelite/emitter.ts @@ -1,3 +1,4 @@ +import { Effect } from 'effect' /** * A hand-instrumented Effect emitter, run as the child under `otelite run` by * the end-to-end test. It reads the receiver endpoint otelite injects and POSTs @@ -10,8 +11,7 @@ * The trace uses the default OTel-SDK JSON dialect the receiver decodes: * 32-hex `traceId`, 16-hex `spanId`, integer `kind`, string int64 nanos. */ -import { FetchHttpClient, HttpClient, HttpClientRequest } from '@effect/platform' -import { Effect } from 'effect' +import { FetchHttpClient, HttpClient, HttpClientRequest } from 'effect/unstable/http' /** Distinctive span name so the test's assertion is unambiguous. */ export const SPAN_NAME = 'otelite-effect-e2e-span' diff --git a/packages/@overeng/utils-dev/src/otelite/errors.ts b/packages/@overeng/utils-dev/src/otelite/errors.ts index ea676dd8db..cd474673c9 100644 --- a/packages/@overeng/utils-dev/src/otelite/errors.ts +++ b/packages/@overeng/utils-dev/src/otelite/errors.ts @@ -7,12 +7,12 @@ import { Schema } from 'effect' */ /** Failed to spawn the `otelite` binary at all (e.g. not on `PATH`). */ -export class OteliteSpawnError extends Schema.TaggedError( +export class OteliteSpawnError extends Schema.TaggedErrorClass( '@overeng/utils-dev/otelite/OteliteSpawnError', )('OteliteSpawnError', { /** The full argv otelite was invoked with. */ argv: Schema.Array(Schema.String), - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }) { override get message(): string { return `failed to spawn otelite (${this.argv.join(' ')})` @@ -24,7 +24,7 @@ export class OteliteSpawnError extends Schema.TaggedError( * happy-path exit code passthrough — otelite's own failures surface as * {@link OteliteCliError} instead, disambiguated by otelite's empty stdout. */ -export class OteliteChildFailed extends Schema.TaggedError( +export class OteliteChildFailed extends Schema.TaggedErrorClass( '@overeng/utils-dev/otelite/OteliteChildFailed', )('OteliteChildFailed', { /** The child's exit code (a signal-killed child is reported as `128 + signo`). */ @@ -42,13 +42,13 @@ export class OteliteChildFailed extends Schema.TaggedError( * missing source, bind/write failure, drain timeout, internal bug). Mapped * from the otelite exit-code table. */ -export class OteliteCliError extends Schema.TaggedError( +export class OteliteCliError extends Schema.TaggedErrorClass( '@overeng/utils-dev/otelite/OteliteCliError', )('OteliteCliError', { /** The raw otelite exit code (one of the `sysexits.h` values). */ exitCode: Schema.Int, /** Symbolic reason mapped from the exit code, or `unknown`. */ - reason: Schema.Literal( + reason: Schema.Literals([ 'usage', // 64 'data-err', // 65 — decode error 'no-input', // 66 — inspect source missing @@ -58,7 +58,7 @@ export class OteliteCliError extends Schema.TaggedError( 'software', // 70 — internal otelite bug 'unimplemented', // 69 'unknown', - ), + ]), argv: Schema.Array(Schema.String), stderr: Schema.optional(Schema.String), }) { @@ -68,11 +68,11 @@ export class OteliteCliError extends Schema.TaggedError( } /** otelite stdout did not decode against the expected `Schema`. */ -export class OteliteDecodeError extends Schema.TaggedError( +export class OteliteDecodeError extends Schema.TaggedErrorClass( '@overeng/utils-dev/otelite/OteliteDecodeError', )('OteliteDecodeError', { /** Which CLI output we tried to decode. */ - kind: Schema.Literal( + kind: Schema.Literals([ 'summary', 'span', 'metric', @@ -80,10 +80,10 @@ export class OteliteDecodeError extends Schema.TaggedError( 'trace-summary', 'metric-summary', 'log-summary', - ), + ]), /** The raw line/stdout that failed to decode. */ raw: Schema.String, - cause: Schema.optional(Schema.Defect), + cause: Schema.optional(Schema.Defect()), }) { override get message(): string { return `failed to decode otelite ${this.kind} output` diff --git a/packages/@overeng/utils-dev/src/otelite/mod.ts b/packages/@overeng/utils-dev/src/otelite/mod.ts index 84cc2f2381..11f28e3bb3 100644 --- a/packages/@overeng/utils-dev/src/otelite/mod.ts +++ b/packages/@overeng/utils-dev/src/otelite/mod.ts @@ -8,7 +8,7 @@ * the error channel. It never reimplements capture/inspect logic. * * Requires a `CommandExecutor` + `FileSystem` in context (e.g. - * `NodeContext.layer` from `@effect/platform-node`) and the `otelite` binary on + * `NodeServices.layer` from `@effect/platform-node`) and the `otelite` binary on * `PATH`, or an absolute binary path in `OTELITE_BIN`. */ export { Otelite } from './Otelite.ts' diff --git a/packages/@overeng/utils-dev/src/otelite/otel.ts b/packages/@overeng/utils-dev/src/otelite/otel.ts index 3a8bd4c5fc..29afdf898d 100644 --- a/packages/@overeng/utils-dev/src/otelite/otel.ts +++ b/packages/@overeng/utils-dev/src/otelite/otel.ts @@ -17,7 +17,7 @@ const OteliteExecAttrs = Schema.Struct({ const OteliteSignalAttrs = Schema.Struct({ label: Schema.NonEmptyString, - signal: Schema.Literal('traces', 'metrics', 'logs'), + signal: Schema.Literals(['traces', 'metrics', 'logs']), }) const encodeLabelAttrs = Schema.decodeSync(OteliteLabelAttrs) diff --git a/packages/@overeng/utils-dev/src/otelite/schema.ts b/packages/@overeng/utils-dev/src/otelite/schema.ts index ed5a9d2744..88ea5ec55b 100644 --- a/packages/@overeng/utils-dev/src/otelite/schema.ts +++ b/packages/@overeng/utils-dev/src/otelite/schema.ts @@ -14,7 +14,7 @@ import { Schema } from 'effect' * - `trace_id` / `span_id` / `parent_span_id` are nullable on row schemas. */ -const Attrs = Schema.Record({ key: Schema.String, value: Schema.String }).annotations({ +const Attrs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: 'Otelite.Attrs', }) @@ -22,7 +22,7 @@ const Attrs = Schema.Record({ key: Schema.String, value: Schema.String }).annota export const Endpoints = Schema.Struct({ http: Schema.String, grpc: Schema.String, -}).annotations({ identifier: 'Otelite.Endpoints' }) +}).annotate({ identifier: 'Otelite.Endpoints' }) /** * `otelite.endpoints/v1` — the FIRST stdout line `capture` emits the instant @@ -35,7 +35,7 @@ export const EndpointsEvent = Schema.Struct({ http: Schema.String, grpc: Schema.String, out: Schema.String, -}).annotations({ identifier: 'Otelite.EndpointsEvent' }) +}).annotate({ identifier: 'Otelite.EndpointsEvent' }) export type EndpointsEvent = typeof EndpointsEvent.Type /** Per-signal capture file paths. */ @@ -43,20 +43,20 @@ export const CaptureFiles = Schema.Struct({ traces: Schema.String, metrics: Schema.String, logs: Schema.String, -}).annotations({ identifier: 'Otelite.CaptureFiles' }) +}).annotate({ identifier: 'Otelite.CaptureFiles' }) /** Per-signal received-record counts. */ export const Counts = Schema.Struct({ spans: Schema.Number, metrics: Schema.Number, logs: Schema.Number, -}).annotations({ identifier: 'Otelite.Counts' }) +}).annotate({ identifier: 'Otelite.Counts' }) /** The child process otelite ran (`run` only; `null` for `capture`). */ export const Child = Schema.Struct({ argv: Schema.Array(Schema.String), exit_code: Schema.Int, -}).annotations({ identifier: 'Otelite.Child' }) +}).annotate({ identifier: 'Otelite.Child' }) /** `otelite.summary/v1` — emitted by `run` and `capture`. */ export const Summary = Schema.Struct({ @@ -67,7 +67,7 @@ export const Summary = Schema.Struct({ counts: Counts, child: Schema.NullOr(Child), duration_ms: Schema.Number, -}).annotations({ identifier: 'Otelite.Summary' }) +}).annotate({ identifier: 'Otelite.Summary' }) export type Summary = typeof Summary.Type /** `otelite.span/v1` — a flattened span row from `inspect --signal traces`. */ @@ -83,7 +83,7 @@ export const SpanRow = Schema.Struct({ duration_ms: Schema.Number, status_code: Schema.Int, attrs: Attrs, -}).annotations({ identifier: 'Otelite.SpanRow' }) +}).annotate({ identifier: 'Otelite.SpanRow' }) export type SpanRow = typeof SpanRow.Type /** `otelite.metric/v1` — a flattened metric data point from `inspect --signal metrics`. */ @@ -99,7 +99,7 @@ export const MetricRow = Schema.Struct({ temporality: Schema.optional(Schema.String), monotonic: Schema.optional(Schema.Boolean), attrs: Attrs, -}).annotations({ identifier: 'Otelite.MetricRow' }) +}).annotate({ identifier: 'Otelite.MetricRow' }) export type MetricRow = typeof MetricRow.Type /** `otelite.log/v1` — a flattened log record from `inspect --signal logs`. */ @@ -114,7 +114,7 @@ export const LogRow = Schema.Struct({ span_id: Schema.NullOr(Schema.String), time_unix_nano: Schema.String, attrs: Attrs, -}).annotations({ identifier: 'Otelite.LogRow' }) +}).annotate({ identifier: 'Otelite.LogRow' }) export type LogRow = typeof LogRow.Type // --- Summary report objects (`--summary`) --- @@ -126,7 +126,7 @@ const DurationGroup = Schema.Struct({ exclusive_duration_ms: Schema.Number, instant_event_span_count: Schema.Number, zero_duration_span_count: Schema.Number, -}).annotations({ identifier: 'Otelite.DurationGroup' }) +}).annotate({ identifier: 'Otelite.DurationGroup' }) const SlowSpan = Schema.Struct({ span_id: Schema.String, @@ -138,7 +138,7 @@ const SlowSpan = Schema.Struct({ exclusive_duration_ms: Schema.Number, relative_start_ms: Schema.Number, instant_event: Schema.Boolean, -}).annotations({ identifier: 'Otelite.SlowSpan' }) +}).annotate({ identifier: 'Otelite.SlowSpan' }) const ErrorSpan = Schema.Struct({ span_id: Schema.String, @@ -148,7 +148,7 @@ const ErrorSpan = Schema.Struct({ inclusive_duration_ms: Schema.Number, exclusive_duration_ms: Schema.Number, instant_event: Schema.Boolean, -}).annotations({ identifier: 'Otelite.ErrorSpan' }) +}).annotate({ identifier: 'Otelite.ErrorSpan' }) /** `otelite.trace-summary/v1` — the per-trace report from `inspect --signal traces --summary`. */ export const TraceSummary = Schema.Struct({ @@ -169,7 +169,7 @@ export const TraceSummary = Schema.Struct({ error_spans: Schema.Array(ErrorSpan), top_labels: Schema.Array(Schema.Struct({ value: Schema.String, count: Schema.Number })), warnings: Schema.Array(Schema.String), -}).annotations({ identifier: 'Otelite.TraceSummary' }) +}).annotate({ identifier: 'Otelite.TraceSummary' }) export type TraceSummary = typeof TraceSummary.Type /** `otelite.metric-summary/v1` — the aggregate report from `inspect --signal metrics --summary`. */ @@ -190,14 +190,14 @@ export const MetricSummary = Schema.Struct({ value_sum: Schema.optional(Schema.Number), }), ), -}).annotations({ identifier: 'Otelite.MetricSummary' }) +}).annotate({ identifier: 'Otelite.MetricSummary' }) export type MetricSummary = typeof MetricSummary.Type /** `otelite.log-summary/v1` — the aggregate report from `inspect --signal logs --summary`. */ export const LogSummary = Schema.Struct({ schema: Schema.Literal('otelite.log-summary/v1'), total: Schema.Number, - by_service: Schema.Record({ key: Schema.String, value: Schema.Number }), - by_severity: Schema.Record({ key: Schema.String, value: Schema.Number }), -}).annotations({ identifier: 'Otelite.LogSummary' }) + by_service: Schema.Record(Schema.String, Schema.Number), + by_severity: Schema.Record(Schema.String, Schema.Number), +}).annotate({ identifier: 'Otelite.LogSummary' }) export type LogSummary = typeof LogSummary.Type diff --git a/packages/@overeng/utils-dev/src/otelite/signal-expect.ts b/packages/@overeng/utils-dev/src/otelite/signal-expect.ts index ab58b33d18..bdecce0ac6 100644 --- a/packages/@overeng/utils-dev/src/otelite/signal-expect.ts +++ b/packages/@overeng/utils-dev/src/otelite/signal-expect.ts @@ -118,7 +118,7 @@ export const telemetryAttr = { _tag: 'Schema', description, matches: (actual) => - Either.isRight(Schema.decodeUnknownEither(Schema.parseJson(schema))(actual)), + Either.isRight(Schema.decodeUnknownEither(Schema.fromJsonString(schema))(actual)), }), } as const diff --git a/packages/@overeng/utils-dev/src/otelite/test-harness.test.ts b/packages/@overeng/utils-dev/src/otelite/test-harness.test.ts index ed71cde27d..c85921ce5e 100644 --- a/packages/@overeng/utils-dev/src/otelite/test-harness.test.ts +++ b/packages/@overeng/utils-dev/src/otelite/test-harness.test.ts @@ -1,6 +1,6 @@ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, expect, it } from '@effect/vitest' +import { FileSystem } from 'effect/FileSystem' import { Effect, Layer, Metric, MetricLabel, Schema } from 'effect' import { TraceJson, writeCaptureDiagnostics } from './diagnostics.ts' @@ -99,13 +99,13 @@ describe('OteliteTestHarness', () => { service: 'otelite-diagnostics', }) - const traceJson = yield* Schema.decodeUnknown(Schema.parseJson(TraceJson))( + const traceJson = yield* Schema.decodeUnknown(Schema.fromJsonString(TraceJson))( yield* fs.readFileString(files.traceJson), ) expect(traceJson.schema).toBe('otelite.trace-json/v1') expect(traceJson.summary.span_count).toBeGreaterThanOrEqual(2) expect(traceJson.spans.some((span) => span.name === 'otelite-diagnostics.child')).toBe(true) - }).pipe(Effect.provide(Layer.mergeAll(OteliteTestHarness.Default, NodeContext.layer))), + }).pipe(Effect.provide(Layer.mergeAll(OteliteTestHarness.Default, NodeServices.layer))), 30_000, ) diff --git a/packages/@overeng/utils-dev/src/otelite/test-harness.ts b/packages/@overeng/utils-dev/src/otelite/test-harness.ts index 67bf95ef28..79503cc4b1 100644 --- a/packages/@overeng/utils-dev/src/otelite/test-harness.ts +++ b/packages/@overeng/utils-dev/src/otelite/test-harness.ts @@ -1,8 +1,8 @@ import { OtlpSerialization, OtlpTracer } from '@effect/opentelemetry' -import * as Otlp from '@effect/opentelemetry/Otlp' -import { FetchHttpClient } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer, type Scope } from 'effect' +import { FetchHttpClient } from 'effect/unstable/http' +import * as Otlp from 'effect/unstable/observability/Otlp' import type { OteliteCliError, OteliteDecodeError, OteliteSpawnError } from './errors.ts' import { withOteliteLabelSpan, withOteliteRootSpan } from './otel.ts' @@ -355,7 +355,7 @@ export class OteliteTestHarness extends Effect.Service()( return { capture } as const }), - dependencies: [Otelite.Default.pipe(Layer.provide(NodeContext.layer))], + dependencies: [Otelite.Default.pipe(Layer.provide(NodeServices.layer))], }, ) {} diff --git a/packages/@overeng/utils-dev/src/otelite/trace-expect.ts b/packages/@overeng/utils-dev/src/otelite/trace-expect.ts index 971c706b6c..62d43a1bd8 100644 --- a/packages/@overeng/utils-dev/src/otelite/trace-expect.ts +++ b/packages/@overeng/utils-dev/src/otelite/trace-expect.ts @@ -95,7 +95,7 @@ export const attr = { _tag: 'Schema', description, matches: (actual) => - Either.isRight(Schema.decodeUnknownEither(Schema.parseJson(schema))(actual)), + Either.isRight(Schema.decodeUnknownEither(Schema.fromJsonString(schema))(actual)), }), } as const diff --git a/packages/@overeng/utils-dev/src/otelite/vitest-bridge.test.ts b/packages/@overeng/utils-dev/src/otelite/vitest-bridge.test.ts index 63e2f342b9..b7f6db71cd 100644 --- a/packages/@overeng/utils-dev/src/otelite/vitest-bridge.test.ts +++ b/packages/@overeng/utils-dev/src/otelite/vitest-bridge.test.ts @@ -1,8 +1,8 @@ import { OtlpSerialization, OtlpTracer } from '@effect/opentelemetry' -import { FetchHttpClient } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { describe, expect, it, layer } from '@effect/vitest' import { Effect, Layer } from 'effect' +import { FetchHttpClient } from 'effect/unstable/http' import { Otelite } from './Otelite.ts' import { otlpTracesUrl } from './otlp-url.ts' @@ -85,7 +85,7 @@ layer(CaptureLayer, { excludeTestServices: true })('OteliteCapture bridge', (it) * nothing lands. The contrasting suffixed path is the main bridge test above * (which DOES capture). Together they prove the suffix is load-bearing. */ -const BareTestLayer = Otelite.Default.pipe(Layer.provideMerge(NodeContext.layer)) +const BareTestLayer = Otelite.Default.pipe(Layer.provideMerge(NodeServices.layer)) describe('OteliteCapture bridge — suffix regression', () => { it.scopedLive( diff --git a/packages/@overeng/utils-dev/src/otelite/vitest-bridge.ts b/packages/@overeng/utils-dev/src/otelite/vitest-bridge.ts index 92d2d4b295..83696bba7a 100644 --- a/packages/@overeng/utils-dev/src/otelite/vitest-bridge.ts +++ b/packages/@overeng/utils-dev/src/otelite/vitest-bridge.ts @@ -22,7 +22,7 @@ * @module */ -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Context, Effect, Layer } from 'effect' import { makeOtelVitestLayer } from '../node-vitest/Vitest.ts' @@ -40,10 +40,9 @@ import type { CaptureHandle, CaptureOptions } from './Otelite.ts' * const spans = yield* cap.inspect({ signal: 'traces', name: mySpanName }) * ``` */ -export class OteliteCapture extends Context.Tag('@overeng/utils-dev/otelite/OteliteCapture')< - OteliteCapture, - CaptureHandle ->() {} +export class OteliteCapture extends Context.Service()( + '@overeng/utils-dev/otelite/OteliteCapture', +) {} /** Options for {@link makeOteliteCaptureLayer}. */ export interface OteliteCaptureLayerOptions extends CaptureOptions { @@ -70,7 +69,7 @@ export interface OteliteCaptureLayerOptions extends CaptureOptions { * the captured receiver. * * Requires `Otelite` + a `CommandExecutor`/`FileSystem` (e.g. - * {@link NodeContext.layer}) in context, both of which it provides internally + * {@link NodeServices.layer}) in context, both of which it provides internally * via {@link Otelite.Default} so the returned layer is self-contained. */ export const makeOteliteCaptureLayer = ( @@ -88,7 +87,7 @@ export const makeOteliteCaptureLayer = ( const otelite = yield* Otelite return yield* otelite.capture(captureOptions) }), - ).pipe(Layer.provide(Otelite.Default), Layer.provide(NodeContext.layer)) + ).pipe(Layer.provide(Otelite.Default), Layer.provide(NodeServices.layer)) // Point the OTLP trace exporter at the captured receiver. Built from the // handle so the URL is the captured endpoint + the locked `/v1/traces` suffix. diff --git a/packages/@overeng/utils/package.json b/packages/@overeng/utils/package.json index b922f1d2be..54318d9492 100644 --- a/packages/@overeng/utils/package.json +++ b/packages/@overeng/utils/package.json @@ -45,26 +45,20 @@ } }, "dependencies": { - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/vitest": "0.29.0", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@noble/hashes": "2.2.0", "@opentelemetry/api": "1.9.1", "@overeng/effect-distributed-lock": "workspace:^", "@overeng/otel-contract": "workspace:^", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "vitest": "4.1.9" }, "devDependencies": { - "@effect/cluster": "0.59.0", - "@effect/experimental": "0.60.0", - "@effect/opentelemetry": "0.63.0", - "@effect/platform": "0.96.2", - "@effect/platform-node": "0.107.0", - "@effect/rpc": "0.75.1", - "@effect/vitest": "0.29.0", - "@effect/workflow": "0.18.2", + "@effect/opentelemetry": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.102", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", @@ -76,26 +70,21 @@ "@playwright/test": "1.61.0", "@storybook/react-vite": "10.4.6", "@types/node": "26.0.0", - "effect": "3.21.4", + "effect": "4.0.0-beta.102", "storybook": "10.4.6", "typescript": "6.0.3", "vite": "8.0.16", "vitest": "4.1.9" }, "peerDependencies": { - "@effect/cluster": "^0.59.0", - "@effect/experimental": "^0.60.0", - "@effect/opentelemetry": "^0.63.0", - "@effect/platform": "^0.96.2", - "@effect/platform-node": "^0.107.0", - "@effect/rpc": "^0.75.1", - "@effect/workflow": "^0.18.2", + "@effect/opentelemetry": "^4.0.0-beta.102", + "@effect/platform-node": "^4.0.0-beta.102", "@playwright/test": "^1.61.0", - "effect": "^3.21.4" + "effect": "^4.0.0-beta.102" }, "pnpm": { "patchedDependencies": { - "@effect/platform@0.96.2": "patches/@effect__platform@0.96.0.patch" + "effect@4.0.0-beta.102": "patches/effect@4.0.0-beta.102.patch" } }, "$genie": { diff --git a/packages/@overeng/utils/package.json.genie.ts b/packages/@overeng/utils/package.json.genie.ts index 0f04273696..33b01ffeed 100644 --- a/packages/@overeng/utils/package.json.genie.ts +++ b/packages/@overeng/utils/package.json.genie.ts @@ -14,17 +14,7 @@ import otelContractPkg from '../otel-contract/package.json.genie.ts' import utilsDevPkg from '../utils-dev/package.json.genie.ts' /** Packages exposed as peer deps (consumers provide) + included in devDeps (for local dev/test) */ -const peerDepNames = [ - '@effect/opentelemetry', - '@effect/experimental', - '@effect/cluster', - '@effect/workflow', - '@effect/platform', - '@effect/platform-node', - '@effect/rpc', - '@playwright/test', - 'effect', -] as const +const peerDepNames = ['@effect/opentelemetry', 'effect', '@effect/platform-node', '@playwright/test'] as const const runtimeDeps = catalog.compose({ workspace: workspaceMember({ memberPath: 'packages/@overeng/utils' }), @@ -35,17 +25,7 @@ const runtimeDeps = catalog.compose({ devDependencies: { workspace: [utilsDevPkg], external: { - ...catalog.pick( - ...peerDepNames, - ...otelSdkDeps, - '@effect/vitest', - '@types/node', - 'storybook', - '@storybook/react-vite', - 'typescript', - 'vite', - 'vitest', - ), + ...catalog.pick(...peerDepNames, ...otelSdkDeps, '@effect/vitest', '@types/node', 'storybook', '@storybook/react-vite', 'typescript', 'vite', 'vitest'), }, }, peerDependencies: { diff --git a/packages/@overeng/utils/patches/effect@4.0.0-beta.102.patch b/packages/@overeng/utils/patches/effect@4.0.0-beta.102.patch new file mode 100644 index 0000000000..5ab89c0ef4 --- /dev/null +++ b/packages/@overeng/utils/patches/effect@4.0.0-beta.102.patch @@ -0,0 +1,77 @@ +diff --git a/dist/unstable/http/HttpClient.js b/dist/unstable/http/HttpClient.js +index 6dc8ad5..28aa227 100644 +--- a/dist/unstable/http/HttpClient.js ++++ b/dist/unstable/http/HttpClient.js +@@ -25,4 +25,8 @@ import * as HttpMethod from "./HttpMethod.js"; + import * as TraceContext from "./HttpTraceContext.js"; + import * as Url from "./Url.js"; ++// LIVE-MIGRATION BRIDGE effect-3-4 B3 - DELETE at contraction - https://github.com/Effect-TS/effect/pull/6697 ++// Preserve effect-utils v3 HTTP client span-attribute allowlist until upstream TracerHeaderFilter is adopted. ++const HTTP_HEADER_ATTR_ALLOWLIST = new Set(["content-type", "content-length", "date", "x-request-id", "x-notion-request-id", "retry-after", "x-ratelimit-remaining"]); ++// LIVE-MIGRATION END effect-3-4 + const TypeId = "~effect/http/HttpClient"; + /** +@@ -262,6 +266,9 @@ export const make = f => makeWith(effect => Effect.flatMap(effect, request => Ef + const redactedHeaderNames = fiber.getRef(Headers.CurrentRedactedNames); + const redactedHeaders = Headers.redact(request.headers, redactedHeaderNames); + for (const name in redactedHeaders) { ++ // LIVE-MIGRATION BRIDGE effect-3-4 B3 - DELETE at contraction - https://github.com/Effect-TS/effect/pull/6697 ++ if (HTTP_HEADER_ATTR_ALLOWLIST.has(name.toLowerCase()) === false) continue; ++ // LIVE-MIGRATION END effect-3-4 + span.attribute(`http.request.header.${name}`, String(redactedHeaders[name])); + } + request = fiber.getRef(TracerPropagationEnabled) ? HttpClientRequest.setHeaders(request, TraceContext.toHeaders(span)) : request; +@@ -272,6 +279,9 @@ export const make = f => makeWith(effect => Effect.flatMap(effect, request => Ef + span.attribute("http.response.status_code", response.status); + const redactedHeaders = Headers.redact(response.headers, redactedHeaderNames); + for (const name in redactedHeaders) { ++ // LIVE-MIGRATION BRIDGE effect-3-4 B3 - DELETE at contraction - https://github.com/Effect-TS/effect/pull/6697 ++ if (HTTP_HEADER_ATTR_ALLOWLIST.has(name.toLowerCase()) === false) continue; ++ // LIVE-MIGRATION END effect-3-4 + span.attribute(`http.response.header.${name}`, String(redactedHeaders[name])); + } + if (scopedController) return Effect.succeed(response); +diff --git a/src/unstable/http/HttpClient.ts b/src/unstable/http/HttpClient.ts +index 3f45ece..e016152 100644 +--- a/src/unstable/http/HttpClient.ts ++++ b/src/unstable/http/HttpClient.ts +@@ -42,6 +42,19 @@ import * as HttpMethod from "./HttpMethod.ts" + import * as TraceContext from "./HttpTraceContext.ts" + import * as Url from "./Url.ts" + ++// LIVE-MIGRATION BRIDGE effect-3-4 B3 - DELETE at contraction - https://github.com/Effect-TS/effect/pull/6697 ++// Preserve effect-utils v3 HTTP client span-attribute allowlist until upstream TracerHeaderFilter is adopted. ++const HTTP_HEADER_ATTR_ALLOWLIST = new Set([ ++ "content-type", ++ "content-length", ++ "date", ++ "x-request-id", ++ "x-notion-request-id", ++ "retry-after", ++ "x-ratelimit-remaining" ++]) ++// LIVE-MIGRATION END effect-3-4 ++ + const TypeId = "~effect/http/HttpClient" + + /** +@@ -841,6 +854,9 @@ export const make = ( + const redactedHeaderNames = fiber.getRef(Headers.CurrentRedactedNames) + const redactedHeaders = Headers.redact(request.headers, redactedHeaderNames) + for (const name in redactedHeaders) { ++ // LIVE-MIGRATION BRIDGE effect-3-4 B3 - DELETE at contraction - https://github.com/Effect-TS/effect/pull/6697 ++ if (HTTP_HEADER_ATTR_ALLOWLIST.has(name.toLowerCase()) === false) continue ++ // LIVE-MIGRATION END effect-3-4 + span.attribute(`http.request.header.${name}`, String(redactedHeaders[name])) + } + request = fiber.getRef(TracerPropagationEnabled) +@@ -854,6 +870,9 @@ export const make = ( + span.attribute("http.response.status_code", response.status) + const redactedHeaders = Headers.redact(response.headers, redactedHeaderNames) + for (const name in redactedHeaders) { ++ // LIVE-MIGRATION BRIDGE effect-3-4 B3 - DELETE at contraction - https://github.com/Effect-TS/effect/pull/6697 ++ if (HTTP_HEADER_ATTR_ALLOWLIST.has(name.toLowerCase()) === false) continue ++ // LIVE-MIGRATION END effect-3-4 + span.attribute(`http.response.header.${name}`, String(redactedHeaders[name])) + } + diff --git a/packages/@overeng/utils/src/browser/BroadcastLogger.ts b/packages/@overeng/utils/src/browser/BroadcastLogger.ts index 483ebea4f1..6f3225d34b 100644 --- a/packages/@overeng/utils/src/browser/BroadcastLogger.ts +++ b/packages/@overeng/utils/src/browser/BroadcastLogger.ts @@ -69,18 +69,7 @@ * * @module */ -import { - Cause, - Effect, - FiberId, - HashMap, - Layer, - Logger, - LogLevel, - Schema, - Scope, - Stream, -} from 'effect' +import { Cause, Effect, HashMap, Layer, Logger, LogLevel, Schema, Scope, Stream } from 'effect' import { OtelAttr, @@ -89,6 +78,9 @@ import { type OtelOperationDefinition, } from '@overeng/otel-contract' +const formatFiberId = (fiberId: unknown): string => + typeof fiberId === 'number' ? `#${fiberId}` : String(fiberId ?? 'unknown') + /** Channel name for broadcasting logs */ export const BROADCAST_CHANNEL_NAME = 'effect-debug-logs' @@ -158,7 +150,7 @@ export class BroadcastLogEntry extends Schema.Class('Broadcas message: Schema.Array(Schema.Unknown), fiberId: Schema.String, spans: Schema.Array(Schema.String), - annotations: Schema.Record({ key: Schema.String, value: Schema.Unknown }), + annotations: Schema.Record(Schema.String, Schema.Unknown), cause: Schema.UndefinedOr(Schema.String), /** Identifies the source (e.g., SharedWorker name or tab ID) */ source: Schema.UndefinedOr(Schema.String), @@ -182,7 +174,7 @@ const makeBroadcastLoggerFromChannel = ({ timestamp: date.getTime(), level: logLevel.label, message: (Array.isArray(message) === true ? message : [message]).map(sanitizeForBroadcast), - fiberId: FiberId.threadName(fiberId), + fiberId: formatFiberId(fiberId), spans: [...spans].map((span) => span.label), annotations: Object.fromEntries( HashMap.toEntries(annotations).map(([k, v]) => [k, sanitizeForBroadcast(v)]), diff --git a/packages/@overeng/utils/src/browser/opfs.ts b/packages/@overeng/utils/src/browser/opfs.ts index 30296bf0b3..042ebd3426 100644 --- a/packages/@overeng/utils/src/browser/opfs.ts +++ b/packages/@overeng/utils/src/browser/opfs.ts @@ -21,7 +21,7 @@ declare global { /** * Error thrown when OPFS is not supported in the current environment. */ -export class OPFSNotSupportedError extends Schema.TaggedError()( +export class OPFSNotSupportedError extends Schema.TaggedErrorClass()( 'OPFSNotSupportedError', { message: Schema.String, @@ -35,7 +35,7 @@ export class OPFSNotSupportedError extends Schema.TaggedError()('OPFSError', { +export class OPFSError extends Schema.TaggedErrorClass()('OPFSError', { operation: Schema.String, path: Schema.optional(Schema.String), cause: Schema.optional(Schema.Unknown), diff --git a/packages/@overeng/utils/src/browser/web-lock.ts b/packages/@overeng/utils/src/browser/web-lock.ts index 2936666676..a386395811 100644 --- a/packages/@overeng/utils/src/browser/web-lock.ts +++ b/packages/@overeng/utils/src/browser/web-lock.ts @@ -12,7 +12,7 @@ import { Deferred, Effect, Runtime, Schema } from 'effect' /** * Error thrown when Web Locks API is not supported. */ -export class WebLockNotSupportedError extends Schema.TaggedError()( +export class WebLockNotSupportedError extends Schema.TaggedErrorClass()( 'WebLockNotSupportedError', { message: Schema.String, diff --git a/packages/@overeng/utils/src/isomorphic/ScopeDebugger.ts b/packages/@overeng/utils/src/isomorphic/ScopeDebugger.ts index af2f26c0f0..3d154799b7 100644 --- a/packages/@overeng/utils/src/isomorphic/ScopeDebugger.ts +++ b/packages/@overeng/utils/src/isomorphic/ScopeDebugger.ts @@ -7,7 +7,7 @@ * - Which finalizers are registered and when */ -import { Effect, Exit, FiberRef, Scope } from 'effect' +import { Context, Effect, Exit, Scope } from 'effect' /** Info about a registered finalizer for debugging */ export interface FinalizerInfo { @@ -28,7 +28,9 @@ export interface FinalizerExecutionInfo extends FinalizerInfo { * FiberRef to track whether scope debugging is enabled for the current fiber. * When enabled, `addTracedFinalizer` will log registration and execution. */ -export const ScopeDebugEnabled = FiberRef.unsafeMake(false) +export const ScopeDebugEnabled = Context.Reference()('ScopeDebugEnabled', { + defaultValue: () => false, +}) /** * Enables scope debugging for an effect and its children. @@ -64,7 +66,7 @@ export const addTracedFinalizer = Effect.fn('ScopeDebugger.addTracedFinalizer')( finalizer: Effect.Effect }) { const { name, finalizer } = opts - const debugEnabled = yield* FiberRef.get(ScopeDebugEnabled) + const debugEnabled = yield* ScopeDebugEnabled const scope = yield* Effect.scope if (debugEnabled === false) { @@ -99,7 +101,7 @@ export const addTracedFinalizer = Effect.fn('ScopeDebugger.addTracedFinalizer')( durationMs, }) }, - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.logError(`Finalizer failed: ${name}`, { finalizer: name, cause, @@ -173,7 +175,7 @@ export const traceFinalizer = Effect.fn('ScopeDebugger.traceFinalizer')(function finalizer: Effect.Effect }) { const { name, finalizer } = opts - const debugEnabled = yield* FiberRef.get(ScopeDebugEnabled) + const debugEnabled = yield* ScopeDebugEnabled if (debugEnabled === false) { yield* finalizer diff --git a/packages/@overeng/utils/src/isomorphic/ScopeDebugger.unit.test.ts b/packages/@overeng/utils/src/isomorphic/ScopeDebugger.unit.test.ts index aea1da9f99..397ac556fa 100644 --- a/packages/@overeng/utils/src/isomorphic/ScopeDebugger.unit.test.ts +++ b/packages/@overeng/utils/src/isomorphic/ScopeDebugger.unit.test.ts @@ -17,10 +17,7 @@ const makeTestLogger = Effect.fnUntraced(function* () { const getLogs = Ref.get(logs).pipe(Effect.map(Chunk.toArray)) - const loggerLayer = Layer.merge( - Logger.replace(Logger.defaultLogger, logger), - Logger.minimumLogLevel(LogLevel.All), - ) + const loggerLayer = Layer.merge(Logger.layer([logger]), Logger.minimumLogLevel(LogLevel.All)) return { logger, getLogs, loggerLayer } as const }) diff --git a/packages/@overeng/utils/src/isomorphic/lineage/mod.ts b/packages/@overeng/utils/src/isomorphic/lineage/mod.ts index 4a96edbd71..53ae176743 100644 --- a/packages/@overeng/utils/src/isomorphic/lineage/mod.ts +++ b/packages/@overeng/utils/src/isomorphic/lineage/mod.ts @@ -21,25 +21,25 @@ import { Option, Schema, type SchemaAST } from 'effect' * -------------------------------------------------------------------------- */ /** A reference to another field, schema, or external system. */ -export const LineageRef = Schema.Union( +export const LineageRef = Schema.Union([ /** Path relative to the root schema, e.g. `$.foo.bar` (same syntax as `SchemaContext`). */ Schema.TaggedStruct('Field', { path: Schema.String }), /** Reference by schema identifier (the `identifier` annotation). */ Schema.TaggedStruct('Schema', { identifier: Schema.String }), /** Foreign-system reference (e.g. `{ system: 'stripe', ref: 'cus_123' }`). */ Schema.TaggedStruct('External', { system: Schema.String, ref: Schema.String }), -) +]) export type LineageRef = typeof LineageRef.Type /** How a `Derived` field is computed from its inputs. */ -export const DerivationKind = Schema.Union( +export const DerivationKind = Schema.Union([ Schema.TaggedStruct('Pure', {}), Schema.TaggedStruct('Aggregation', { - op: Schema.Literal('sum', 'count', 'min', 'max', 'avg', 'custom'), + op: Schema.Literals(['sum', 'count', 'min', 'max', 'avg', 'custom']), }), Schema.TaggedStruct('Reduction', { description: Schema.String }), Schema.TaggedStruct('External', { service: Schema.String }), -) +]) export type DerivationKind = typeof DerivationKind.Type /** @@ -47,7 +47,7 @@ export type DerivationKind = typeof DerivationKind.Type * * @see https://github.com/overengineeringstudio/effect-utils/issues/687 */ -export const Lineage = Schema.Union( +export const Lineage = Schema.Union([ Schema.TaggedStruct('SourceOfTruth', { owner: Schema.optional(Schema.String), system: Schema.optional(Schema.String), @@ -77,7 +77,7 @@ export const Lineage = Schema.Union( fn: Schema.optional(Schema.String), description: Schema.optional(Schema.String), }), -) +]) export type Lineage = typeof Lineage.Type /** Who can read/write this field. Composable with any `Lineage`. */ @@ -89,7 +89,7 @@ export type Authority = typeof Authority.Type /** Temporal freshness of a captured value. */ export const Freshness = Schema.Struct({ - capturedAt: Schema.optional(Schema.Literal('now', 'event-time', 'snapshot')), + capturedAt: Schema.optional(Schema.Literals(['now', 'event-time', 'snapshot'])), maxAgeMs: Schema.optional(Schema.Number), }) export type Freshness = typeof Freshness.Type @@ -235,7 +235,7 @@ const coerceDerivationKind = ( const annotate = (args: { id: symbol; value: V }) => (schema: S): S => - schema.annotations({ [args.id]: args.value }) as S + schema.annotate({ [args.id]: args.value }) as S const lineageAnnotation = (value: Lineage) => diff --git a/packages/@overeng/utils/src/isomorphic/lineage/mod.unit.test.ts b/packages/@overeng/utils/src/isomorphic/lineage/mod.unit.test.ts index 09b3a19196..eeedf75efa 100644 --- a/packages/@overeng/utils/src/isomorphic/lineage/mod.unit.test.ts +++ b/packages/@overeng/utils/src/isomorphic/lineage/mod.unit.test.ts @@ -107,7 +107,7 @@ describe('Lineage annotations: round-trip', () => { describe('Lineage annotations: fail-soft', () => { it('returns undefined for malformed annotation payloads', () => { - const bogus = Schema.Number.annotations({ [LineageAnnotationId]: { _tag: 'Bogus' } }) + const bogus = Schema.Number.annotate({ [LineageAnnotationId]: { _tag: 'Bogus' } }) expect(getLineage(bogus)).toBeUndefined() }) diff --git a/packages/@overeng/utils/src/isomorphic/mod.ts b/packages/@overeng/utils/src/isomorphic/mod.ts index 6df40a6792..1da4545e62 100644 --- a/packages/@overeng/utils/src/isomorphic/mod.ts +++ b/packages/@overeng/utils/src/isomorphic/mod.ts @@ -14,7 +14,7 @@ import { Schema } from 'effect' * Generic error class for wrapping unknown error causes. * Useful when catching errors of unknown type and wrapping them in a typed Effect error. */ -export class UnknownError extends Schema.TaggedError()('UnknownError', { +export class UnknownError extends Schema.TaggedErrorClass()('UnknownError', { cause: Schema.Unknown, payload: Schema.optional(Schema.Unknown), }) {} diff --git a/packages/@overeng/utils/src/isomorphic/timestamp.ts b/packages/@overeng/utils/src/isomorphic/timestamp.ts index 254e9a047f..1f5750589f 100644 --- a/packages/@overeng/utils/src/isomorphic/timestamp.ts +++ b/packages/@overeng/utils/src/isomorphic/timestamp.ts @@ -15,11 +15,10 @@ export const timestamp = (value: number | string | Date): Timestamp => { } /** Schema that transforms between Timestamp and plain number */ -export const timestampSchema = Schema.transform( - Schema.fromBrand(Brand.nominal())(Schema.Number), - Schema.Number, - { decode: (_) => _, encode: timestamp }, -) +export const timestampSchema = Schema.fromBrand( + 'Timestamp', + Brand.nominal(), +)(Schema.Number).pipe(Schema.decodeTo(Schema.Number, { decode: (_) => _, encode: timestamp })) /** Returns the current time as a Timestamp */ export const timestampNow = (): Timestamp => Math.round(Date.now()) as Timestamp diff --git a/packages/@overeng/utils/src/node/ActiveHandlesDebugger.unit.test.ts b/packages/@overeng/utils/src/node/ActiveHandlesDebugger.unit.test.ts index 141f55d3c0..0878ea0d3b 100644 --- a/packages/@overeng/utils/src/node/ActiveHandlesDebugger.unit.test.ts +++ b/packages/@overeng/utils/src/node/ActiveHandlesDebugger.unit.test.ts @@ -22,10 +22,7 @@ const makeTestLogger = Effect.fnUntraced(function* () { const getLogs = Ref.get(logs).pipe(Effect.map(Chunk.toArray)) - const loggerLayer = Layer.merge( - Logger.replace(Logger.defaultLogger, logger), - Logger.minimumLogLevel(LogLevel.All), - ) + const loggerLayer = Layer.merge(Logger.layer([logger]), Logger.minimumLogLevel(LogLevel.All)) return { getLogs, loggerLayer } as const }) diff --git a/packages/@overeng/utils/src/node/FileLogger.ts b/packages/@overeng/utils/src/node/FileLogger.ts index 410df15d80..98a975c3ca 100644 --- a/packages/@overeng/utils/src/node/FileLogger.ts +++ b/packages/@overeng/utils/src/node/FileLogger.ts @@ -2,20 +2,12 @@ import * as fs from 'node:fs' import path from 'node:path' import util from 'node:util' -import { - Cause, - Effect, - FiberId, - HashMap, - Inspectable, - Layer, - List, - Logger, - type LogLevel, - LogSpan, -} from 'effect' +import { Cause, Effect, HashMap, Inspectable, Layer, Logger, type LogLevel } from 'effect' import * as EffectArray from 'effect/Array' +const formatFiberId = (fiberId: unknown): string => + typeof fiberId === 'number' ? `#${fiberId}` : String(fiberId ?? 'unknown') + /** * Creates a Layer that replaces the default logger with a pretty-printed file logger. * @@ -63,15 +55,14 @@ export const makeFileLogger = ({ logFilePath, threadName, colors }: MakeFileLogg (fd) => Effect.sync(() => fs.closeSync(fd)), ) - return Logger.replace( - Logger.defaultLogger, + return Logger.layer([ prettyLoggerTty({ colors: colors ?? false, stderr: false, formatDate: (date) => `${defaultDateFormat(date)} ${threadName ?? ''}`, onLog: (str) => fs.writeSync(logFile, str), }), - ) + ]) }), ) @@ -185,13 +176,12 @@ export const prettyLoggerTty = (options: { let firstLine = color(`[${options.formatDate(date)}]`, colors.white) + ` ${color(logLevel.label, ...logLevelColors[logLevel._tag])}` + - ` (${FiberId.threadName(fiberId)})` + ` (${formatFiberId(fiberId)})` - if (List.isCons(spans) === true) { + if (spans.length > 0) { const now = date.getTime() - const render = LogSpan.render(now) - for (const span of spans) { - firstLine += ` ${render(span)}` + for (const [label, timestamp] of spans) { + firstLine += ` ${label} (${now - timestamp}ms)` } } diff --git a/packages/@overeng/utils/src/node/cli-version.ts b/packages/@overeng/utils/src/node/cli-version.ts index 2cbbc0ac8b..1612f1fdfd 100644 --- a/packages/@overeng/utils/src/node/cli-version.ts +++ b/packages/@overeng/utils/src/node/cli-version.ts @@ -7,7 +7,7 @@ export interface CliVersionInfo { } /** CLI identity and version, provided at startup for error diagnostics. */ -export class CliVersion extends Context.Tag('CliVersion')() { +export class CliVersion extends Context.Service()('CliVersion') { /** * Yield a version suffix for use in error messages. * Returns e.g. `" (genie 0.1.0+abc123)"` or `""` if `CliVersion` is not provided. diff --git a/packages/@overeng/utils/src/node/cmd-log.ts b/packages/@overeng/utils/src/node/cmd-log.ts index 3c810fdcfa..fef1f8c403 100644 --- a/packages/@overeng/utils/src/node/cmd-log.ts +++ b/packages/@overeng/utils/src/node/cmd-log.ts @@ -38,7 +38,7 @@ export const prepareCmdLogging: ( const archivedBase = `${path.parse(logFileName).name}-${safeIso}.log` const archivedLog = path.join(archiveDir, archivedBase) yield* Effect.try(() => fs.renameSync(currentLogPath, archivedLog)).pipe( - Effect.catchAll(() => + Effect.catch(() => Effect.try(() => { fs.copyFileSync(currentLogPath, archivedLog) fs.truncateSync(currentLogPath, 0) diff --git a/packages/@overeng/utils/src/node/cmd.contract.ts b/packages/@overeng/utils/src/node/cmd.contract.ts index 468e33efe7..eafec163f3 100644 --- a/packages/@overeng/utils/src/node/cmd.contract.ts +++ b/packages/@overeng/utils/src/node/cmd.contract.ts @@ -64,7 +64,7 @@ export const CmdArgs = OtelAttr.json({ key: 'cmd.args', schema: Schema.Array(Schema.String), metadata: { cardinality: 'high' }, -}).annotations({ +}).annotate({ [WeaverAttrAnnotationId]: { brief: 'Command arguments (JSON-encoded array of strings).', stability: 'development', diff --git a/packages/@overeng/utils/src/node/cmd.ts b/packages/@overeng/utils/src/node/cmd.ts index e519ac3073..b636d5dd51 100644 --- a/packages/@overeng/utils/src/node/cmd.ts +++ b/packages/@overeng/utils/src/node/cmd.ts @@ -1,9 +1,5 @@ import fs from 'node:fs' -import * as Command from '@effect/platform/Command' -import type * as CommandExecutor from '@effect/platform/CommandExecutor' -import type { Process } from '@effect/platform/CommandExecutor' -import type { PlatformError } from '@effect/platform/Error' import type { Scope } from 'effect' import { Cause, @@ -11,16 +7,17 @@ import { type Duration, Effect, Fiber, - FiberId, - FiberRefs, HashMap, identity, - List, LogLevel, Option, Schema, Stream, } from 'effect' +import type { PlatformError } from 'effect/Error' +import { ChildProcess as Command } from 'effect/unstable/process' +import type * as CommandExecutor from 'effect/unstable/process/CommandExecutor' +import type { Process } from 'effect/unstable/process/CommandExecutor' import { type OtelAttrEncodeError, type OtelOperationDefinition } from '@overeng/otel-contract' @@ -422,21 +419,21 @@ export const cmdCollect = (opts: { ) /** Internal error for process signal operations */ -class ProcessSignalError extends Schema.TaggedError()('ProcessSignalError', { - cause: Schema.Defect, - code: Schema.optionalWith(Schema.String, { as: 'Option' }), -}) {} +class ProcessSignalError extends Schema.TaggedErrorClass()( + 'ProcessSignalError', + { + cause: Schema.Defect(), + code: Schema.OptionFromOptional(Schema.String), + }, +) {} /** Error thrown when a shell command exits with non-zero status */ -export class CmdError extends Schema.TaggedError()('CmdError', { +export class CmdError extends Schema.TaggedErrorClass()('CmdError', { command: Schema.String, args: Schema.Array(Schema.String), cwd: Schema.String, - env: Schema.Record({ - key: Schema.String, - value: Schema.String.pipe(Schema.UndefinedOr), - }), - stderr: Schema.Literal('inherit', 'pipe'), + env: Schema.Record(Schema.String, Schema.UndefinedOr(Schema.String)), + stderr: Schema.Literals(['inherit', 'pipe']), }) {} type TRunBaseArgs = { @@ -507,12 +504,11 @@ const runWithLogging = ({ const appendLog = ({ channel, content }: { channel: 'stdout' | 'stderr'; content: string }) => Effect.sync(() => { const formatted = prettyLogger.log({ - fiberId: FiberId.none, + fiberId: undefined, logLevel: channel === 'stdout' ? LogLevel.Info : LogLevel.Warning, message: [`[${channel}]${content.length > 0 ? ` ${content}` : ''}`], cause: Cause.empty, - context: FiberRefs.empty(), - spans: List.empty(), + spans: [], annotations: HashMap.empty(), date: new Date(), }) @@ -623,7 +619,7 @@ const sendSignalToProcessGroup = (opts: { }) }, }).pipe( - Effect.catchAll((e) => { + Effect.catch((e) => { // ESRCH = no such process (already dead) - that's fine if (Option.getOrUndefined(e.code) === 'ESRCH') return Effect.void // Other errors: fall back to individual kill (ignore errors) diff --git a/packages/@overeng/utils/src/node/cmd.unit.test.ts b/packages/@overeng/utils/src/node/cmd.unit.test.ts index ecc3fe8747..6a50d0e38f 100644 --- a/packages/@overeng/utils/src/node/cmd.unit.test.ts +++ b/packages/@overeng/utils/src/node/cmd.unit.test.ts @@ -1,8 +1,8 @@ import fs from 'node:fs' import path from 'node:path' -import { NodeContext } from '@effect/platform-node' -import * as CommandExecutor from '@effect/platform/CommandExecutor' +import { NodeServices } from '@effect/platform-node' +import * as CommandExecutor from 'effect/unstable/process/CommandExecutor' import { Effect, Layer } from 'effect' import { expect } from 'vitest' @@ -12,7 +12,7 @@ import { shouldNeverHappen } from '../isomorphic/mod.ts' import { cmd, cmdCollect } from './cmd.ts' import { CurrentWorkingDirectory } from './workspace.ts' -const TestLayer = Layer.mergeAll(NodeContext.layer, CurrentWorkingDirectory.live) +const TestLayer = Layer.mergeAll(NodeServices.layer, CurrentWorkingDirectory.live) Vitest.describe('cmd helper', () => { const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g') diff --git a/packages/@overeng/utils/src/node/file-system-backing.ts b/packages/@overeng/utils/src/node/file-system-backing.ts index 64091d572e..a25fa28d6f 100644 --- a/packages/@overeng/utils/src/node/file-system-backing.ts +++ b/packages/@overeng/utils/src/node/file-system-backing.ts @@ -1,5 +1,16 @@ -import { FileSystem, Path } from '@effect/platform' -import { Cause, Data, Duration, Effect, Layer, Option, Schema, Stream } from 'effect' +import { + Cause, + Data, + Duration, + Effect, + Layer, + Option, + Result, + Schema, + Stream, +} from 'effect' +import * as FileSystem from 'effect/FileSystem' +import * as Path from 'effect/Path' import { DistributedSemaphoreBacking, @@ -59,6 +70,14 @@ const isNotFoundError = (cause: unknown): boolean => { if (typeof cause !== 'object' || cause === null) return false const record = cause as Record if (record._tag === 'SystemError' && record.reason === 'NotFound') return true + if ( + record._tag === 'PlatformError' && + typeof record.reason === 'object' && + record.reason !== null && + (record.reason as Record)._tag === 'NotFound' + ) { + return true + } if (record.code === 'ENOENT') return true return false } @@ -74,13 +93,14 @@ const readHolderLock = Effect.fn('FileSystemBacking.readHolderLock')(function* ( const fs = yield* FileSystem.FileSystem const content = yield* fs.readFileString(filePath).pipe( - Effect.catchAllCause((cause) => { - const failure = Cause.failureOption(cause).pipe(Option.getOrUndefined) + Effect.catchCause((cause) => { + const failure = Cause.findErrorOption(cause).pipe(Option.getOrUndefined) if (failure !== undefined && isNotFoundError(failure) === true) { return Effect.void } - const defect = Cause.dieOption(cause).pipe(Option.getOrUndefined) + const defectResult = Cause.findDefect(cause) + const defect = Result.isSuccess(defectResult) ? defectResult.success : undefined if (defect !== undefined && isNotFoundError(defect) === true) { return Effect.void } @@ -105,7 +125,7 @@ const readHolderLock = Effect.fn('FileSystemBacking.readHolderLock')(function* ( return undefined } - const parsed = yield* Schema.decodeUnknown(Schema.parseJson(HolderLockSchema))(content).pipe( + const parsed = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(HolderLockSchema))(content).pipe( Effect.mapError((cause) => new SemaphoreBackingError({ operation: 'parseJson', cause })), ) @@ -138,7 +158,7 @@ const writeHolderLock = Effect.fn('FileSystemBacking.writeHolderLock')(function* Effect.mapError((cause) => new SemaphoreBackingError({ operation: 'makeDirectory', cause })), ) - const json = yield* Schema.encode(Schema.parseJson(HolderLockSchema))(content).pipe( + const json = yield* Schema.encodeEffect(Schema.fromJsonString(HolderLockSchema))(content).pipe( Effect.mapError((cause) => new SemaphoreBackingError({ operation: 'encodeJson', cause })), ) @@ -376,7 +396,7 @@ export const layer = ( return fs.watch(keyDir).pipe( Stream.filter((event) => event._tag === 'Update' || event._tag === 'Remove'), Stream.map((): void => undefined), - Stream.catchAll(() => Stream.empty), + Stream.catch(() => Stream.empty), ) }), ) diff --git a/packages/@overeng/utils/src/node/file-system-backing.unit.test.ts b/packages/@overeng/utils/src/node/file-system-backing.unit.test.ts index 922de14b2e..7bd455fa65 100644 --- a/packages/@overeng/utils/src/node/file-system-backing.unit.test.ts +++ b/packages/@overeng/utils/src/node/file-system-backing.unit.test.ts @@ -2,8 +2,9 @@ import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' -import { FileSystem, Path } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' +import * as FileSystem from 'effect/FileSystem' +import * as Path from 'effect/Path' import { Context, Data, Deferred, Duration, Effect, Fiber, Layer, Schema, Stream } from 'effect' import { expect } from 'vitest' @@ -594,15 +595,15 @@ Vitest.describe('FileSystemBacking', () => { expect(entries).toEqual(['holder-a.lock', 'holder-b.lock']) // Verify lock file content - const holderAContent = yield* Schema.decodeUnknown(Schema.parseJson(LockFileContent))( - fs.readFileSync(`${keyDir}/holder-a.lock`, 'utf-8'), - ) + const holderAContent = yield* Schema.decodeUnknown( + Schema.fromJsonString(LockFileContent), + )(fs.readFileSync(`${keyDir}/holder-a.lock`, 'utf-8')) expect(holderAContent.permits).toBe(2) expect(typeof holderAContent.expiresAt).toBe('number') - const holderBContent = yield* Schema.decodeUnknown(Schema.parseJson(LockFileContent))( - fs.readFileSync(`${keyDir}/holder-b.lock`, 'utf-8'), - ) + const holderBContent = yield* Schema.decodeUnknown( + Schema.fromJsonString(LockFileContent), + )(fs.readFileSync(`${keyDir}/holder-b.lock`, 'utf-8')) expect(holderBContent.permits).toBe(1) }).pipe(Effect.provide(backingLayer)) }).pipe(Effect.provide(TestLayer), Effect.scoped), @@ -829,7 +830,7 @@ Vitest.describe('FileSystemBacking', () => { const now = Date.now() yield* fsService.makeDirectory(keyDir, { recursive: true }) - const expiredLockContent = yield* Schema.encode(Schema.parseJson(LockFileContent))({ + const expiredLockContent = yield* Schema.encode(Schema.fromJsonString(LockFileContent))({ permits: 2, expiresAt: now - 60_000, }) @@ -837,7 +838,7 @@ Vitest.describe('FileSystemBacking', () => { `${keyDir}/${encodeURIComponent('holder-expired')}.lock`, expiredLockContent, ) - const activeLockContent = yield* Schema.encode(Schema.parseJson(LockFileContent))({ + const activeLockContent = yield* Schema.encode(Schema.fromJsonString(LockFileContent))({ permits: 3, expiresAt: now + 60_000, }) @@ -1007,7 +1008,7 @@ Vitest.describe('FileSystemBacking', () => { // it is the signal that the compatibility shim (register entry // filesystem-watch-recursive-option-removed) is still owed. expect(observedPathNames).not.toContain(nestedChild) - }).pipe(Effect.provide(NodeContext.layer), Effect.scoped), + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ) }) @@ -1035,7 +1036,7 @@ Vitest.describe('FileSystemBacking', () => { // Stream should complete (not hang) — the 5s timeout is a safety net yield* stream.pipe(Stream.runDrain, Effect.timeout(Duration.seconds(5))) }).pipe(Effect.provide(backingLayer)) - }).pipe(Effect.provide(NodeContext.layer), Effect.scoped), + }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ) }) }) diff --git a/packages/@overeng/utils/src/node/otel-attrs.unit.test.ts b/packages/@overeng/utils/src/node/otel-attrs.unit.test.ts index c0f3932881..6082948566 100644 --- a/packages/@overeng/utils/src/node/otel-attrs.unit.test.ts +++ b/packages/@overeng/utils/src/node/otel-attrs.unit.test.ts @@ -14,12 +14,12 @@ import { describe('OtelAttrs', () => { it('derives primitive, literal, uuid, option, date, duration, and explicit array attributes', async () => { const Attrs = Schema.Struct({ - label: Schema.NonEmptyTrimmedString.pipe(OtelAttr.spanLabel()), + label: Schema.Trimmed.check(Schema.isNonEmpty()).pipe(OtelAttr.spanLabel()), requestId: Schema.UUID.pipe(OtelAttr.key({ key: 'request.id' })), - outcome: Schema.Literal('approved', 'denied', 'timeout').pipe( + outcome: Schema.Literals(['approved', 'denied', 'timeout']).pipe( OtelAttr.key({ key: 'op.outcome' }), ), - count: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: 'op.count' })), + count: Schema.Natural.pipe(OtelAttr.key({ key: 'op.count' })), cacheHit: Schema.Boolean.pipe(OtelAttr.key({ key: 'op.cache_hit' })), maybeShard: Schema.OptionFromNullOr(Schema.String).pipe(OtelAttr.key({ key: 'op.shard' })), at: Schema.DateTimeUtc.pipe(OtelAttr.key({ key: 'op.at' })), @@ -195,8 +195,8 @@ describe('OtelAttrs', () => { asJson: Schema.Struct({ id: Schema.String }).pipe( OtelAttr.key({ key: 'json', encode: 'json' }), ), - asString: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: 'string', encode: 'string' })), - asNumber: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: 'number', encode: 'number' })), + asString: Schema.Natural.pipe(OtelAttr.key({ key: 'string', encode: 'string' })), + asNumber: Schema.Natural.pipe(OtelAttr.key({ key: 'number', encode: 'number' })), asBoolean: Schema.Boolean.pipe(OtelAttr.key({ key: 'boolean', encode: 'boolean' })), secret: Schema.Redacted(Schema.String).pipe( OtelAttr.key({ key: 'secret', encode: 'redacted' }), @@ -260,7 +260,7 @@ describe('OtelAttrs', () => { OtelAttrs.define( Schema.Struct({ label: Schema.String.pipe(OtelAttr.spanLabel()), - count: Schema.NonNegativeInt.pipe(OtelAttr.key({ key: 'retry.count' })), + count: Schema.Natural.pipe(OtelAttr.key({ key: 'retry.count' })), }), ), ) diff --git a/packages/@overeng/utils/src/node/otel-identity.test.ts b/packages/@overeng/utils/src/node/otel-identity.test.ts index 0ca26db9fa..2898693074 100644 --- a/packages/@overeng/utils/src/node/otel-identity.test.ts +++ b/packages/@overeng/utils/src/node/otel-identity.test.ts @@ -9,9 +9,9 @@ * assertions read the raw per-signal `*.ndjson` and walk the resource block. */ -import { FileSystem } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer, Metric, Option, Schema, type Scope } from 'effect' +import * as FileSystem from 'effect/FileSystem' import { expect } from 'vitest' import { ServiceIdentity } from '@overeng/otel-contract' @@ -74,7 +74,7 @@ const resourceAttrsBySignal = ( const out: Record> = {} // @effect-diagnostics-next-line schemaSyncInEffect:off -- reads the tool's own capture ndjson in a controlled test env; a malformed line is a scaffolding bug, so a thrown defect is correct (this helper is annotated `Effect<..., never, FileSystem>`). const parseLine = Schema.decodeSync( - Schema.parseJson(Schema.Record({ key: Schema.String, value: Schema.Array(Schema.Object) })), + Schema.fromJsonString(Schema.Record(Schema.String, Schema.Array(Schema.Object))), ) for (const [signal, resourceKey] of files) { const raw = yield* fs @@ -143,7 +143,7 @@ Vitest.describe('makeOtelCliLayer — typed ServiceIdentity', () => { expect(attrs[signal]?.['service.namespace']).toBe('overeng.test') expect(attrs[signal]?.['service.version']).toBe('9.9.9') } - }).pipe(Effect.provide(Layer.provideMerge(Otelite.Default, NodeContext.layer))), + }).pipe(Effect.provide(Layer.provideMerge(Otelite.Default, NodeServices.layer))), ) Vitest.it.scoped( @@ -171,7 +171,7 @@ Vitest.describe('makeOtelCliLayer — typed ServiceIdentity', () => { expect(attrs['traces']?.['deployment.environment']).toBe('ci-test') // Explicit identity wins over the colliding env value. expect(attrs['traces']?.['service.namespace']).toBe('overeng.test') - }).pipe(Effect.provide(Layer.provideMerge(Otelite.Default, NodeContext.layer))), + }).pipe(Effect.provide(Layer.provideMerge(Otelite.Default, NodeServices.layer))), ) Vitest.it.effect('rejects a raw-string name on the typed identity path', () => diff --git a/packages/@overeng/utils/src/node/otel-telemetry.test.ts b/packages/@overeng/utils/src/node/otel-telemetry.test.ts index 3f41a47280..e467d297e2 100644 --- a/packages/@overeng/utils/src/node/otel-telemetry.test.ts +++ b/packages/@overeng/utils/src/node/otel-telemetry.test.ts @@ -13,7 +13,7 @@ * absent-layer case). */ -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import { Effect, Layer, Metric, Option, Schema } from 'effect' import { expect } from 'vitest' @@ -89,7 +89,7 @@ Vitest.describe('withTelemetry — typed front door', () => { expect(rows.length).toBeGreaterThan(0) expect(rows.every((r) => r.service === SERVICE)).toBe(true) } - }).pipe(Effect.provide(Layer.provideMerge(Otelite.Default, NodeContext.layer))), + }).pipe(Effect.provide(Layer.provideMerge(Otelite.Default, NodeServices.layer))), ) }) @@ -116,7 +116,7 @@ Vitest.describe('sampleGauge / telemetryEnabled — sampler primitive', () => { type: 'gauge', value: 1234, }) - }).pipe(Effect.provide(Layer.provideMerge(Otelite.Default, NodeContext.layer))), + }).pipe(Effect.provide(Layer.provideMerge(Otelite.Default, NodeServices.layer))), ) Vitest.it.scoped('is a true no-op (never reads, never forks) when telemetry is off', () => diff --git a/packages/@overeng/utils/src/node/otel.ts b/packages/@overeng/utils/src/node/otel.ts index 31b15eba94..96b9f4b48e 100644 --- a/packages/@overeng/utils/src/node/otel.ts +++ b/packages/@overeng/utils/src/node/otel.ts @@ -12,8 +12,6 @@ * @module */ -import * as Otlp from '@effect/opentelemetry/Otlp' -import { FetchHttpClient } from '@effect/platform' import type { MetricLabel } from 'effect' import { Clock, @@ -28,6 +26,8 @@ import { type Scope, Tracer, } from 'effect' +import { FetchHttpClient } from 'effect/unstable/http' +import * as Otlp from 'effect/unstable/observability/Otlp' import { ServiceIdentity } from '@overeng/otel-contract' @@ -51,10 +51,10 @@ export * from './otel-attrs.ts' * it with {@link Effect.serviceOption} so commands stay runnable without the * layer (absent tag ≡ telemetry disabled), keeping the tag out of their `R`. */ -export class OtelConfig extends Context.Tag('@overeng/utils/OtelConfig')< +export class OtelConfig extends Context.Service< OtelConfig, { readonly endpoint: Option.Option } ->() {} +>()('@overeng/utils/OtelConfig') {} /** * Resolve the OTLP endpoint at a binary's composition root via Effect `Config`, @@ -209,7 +209,7 @@ const defaultShutdownTimeoutMs = (): number => (process.stdout.isTTY === true ? * name: 'my-cli', namespace: 'overeng', version, * }) * const baseLayer = Layer.mergeAll( - * NodeContext.layer, + * NodeServices.layer, * makeOtelCliLayer({ identity }), * ) * diff --git a/packages/@overeng/utils/src/node/playwright/op.ts b/packages/@overeng/utils/src/node/playwright/op.ts index 9cbd1a1b4d..7ffa258117 100644 --- a/packages/@overeng/utils/src/node/playwright/op.ts +++ b/packages/@overeng/utils/src/node/playwright/op.ts @@ -23,11 +23,11 @@ import { PwExpectAttrs, PwOp, PwTryAttrs } from './pw.contract.ts' * Any direct call into Playwright that can fail due to a closed page/context or timing issues should * be wrapped so tests receive structured errors and spans. */ -export class PwOpError extends Schema.TaggedError()('PwOpError', { +export class PwOpError extends Schema.TaggedErrorClass()('PwOpError', { /** Stable operation identifier (e.g. `pw.page.goto`, `pw.context.cookies`). */ op: Schema.String, /** Underlying Playwright/Node defect. */ - cause: Schema.Defect, + cause: Schema.Defect(), }) {} // DYNAMIC-NAME BRIDGE: the span name is the runtime `op` string, so it has no stable single-signal diff --git a/packages/@overeng/utils/src/node/playwright/otel.ts b/packages/@overeng/utils/src/node/playwright/otel.ts index 0e5fb072f6..155f5f9742 100644 --- a/packages/@overeng/utils/src/node/playwright/otel.ts +++ b/packages/@overeng/utils/src/node/playwright/otel.ts @@ -13,8 +13,8 @@ */ import { OtlpSerialization, OtlpTracer, Tracer } from '@effect/opentelemetry' -import { FetchHttpClient } from '@effect/platform' import { Effect, Layer, Schema } from 'effect' +import { FetchHttpClient } from 'effect/unstable/http' /** * Minimal parent span context needed to join an existing trace across process boundaries. @@ -28,7 +28,7 @@ import { Effect, Layer, Schema } from 'effect' export const ParentSpanContextSchema = Schema.Struct({ traceId: Schema.String, spanId: Schema.String, -}).annotations({ identifier: 'ParentSpanContext' }) +}).annotate({ identifier: 'ParentSpanContext' }) export type ParentSpanContext = typeof ParentSpanContextSchema.Type @@ -47,7 +47,7 @@ export const currentParentSpanContextJson: Effect.Effect { Vitest.it.effect( 'returns ExternalSpan for valid JSON', Effect.fnUntraced(function* () { - const validContext = yield* Schema.encode(Schema.parseJson(ParentSpanContextSchema))({ + const validContext = yield* Schema.encode(Schema.fromJsonString(ParentSpanContextSchema))({ traceId: '0af7651916cd43dd8448eb211c80319c', spanId: 'b7ad6b7169203331', }) @@ -121,7 +121,7 @@ Vitest.describe('playwright/otel', () => { Vitest.it.effect( 'returns ExternalSpan for valid JSON from custom env var', Effect.fnUntraced(function* () { - const validContext = yield* Schema.encode(Schema.parseJson(ParentSpanContextSchema))({ + const validContext = yield* Schema.encode(Schema.fromJsonString(ParentSpanContextSchema))({ traceId: 'custom-trace-id', spanId: 'custom-span-id', }) @@ -152,7 +152,7 @@ Vitest.describe('playwright/otel', () => { 'dies on malformed context (missing traceId)', Effect.fnUntraced(function* () { const malformedContext = yield* Schema.encode( - Schema.parseJson(Schema.Struct({ spanId: Schema.String })), + Schema.fromJsonString(Schema.Struct({ spanId: Schema.String })), )({ spanId: 'b7ad6b7169203331' }) process.env[PW_SPAN_CONTEXT_ENV_VAR] = malformedContext @@ -167,7 +167,7 @@ Vitest.describe('playwright/otel', () => { 'dies on malformed context (missing spanId)', Effect.fnUntraced(function* () { const malformedContext = yield* Schema.encode( - Schema.parseJson(Schema.Struct({ traceId: Schema.String })), + Schema.fromJsonString(Schema.Struct({ traceId: Schema.String })), )({ traceId: '0af7651916cd43dd8448eb211c80319c' }) process.env[PW_SPAN_CONTEXT_ENV_VAR] = malformedContext @@ -211,7 +211,9 @@ Vitest.describe('playwright/otel', () => { 'uses custom parent span env var', Effect.fnUntraced(function* () { delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT - const customParentSpan = yield* Schema.encode(Schema.parseJson(ParentSpanContextSchema))({ + const customParentSpan = yield* Schema.encode( + Schema.fromJsonString(ParentSpanContextSchema), + )({ traceId: '0af7651916cd43dd8448eb211c80319c', spanId: 'b7ad6b7169203331', }) diff --git a/packages/@overeng/utils/src/node/playwright/tags.ts b/packages/@overeng/utils/src/node/playwright/tags.ts index d674fd5e1b..ac2224c856 100644 --- a/packages/@overeng/utils/src/node/playwright/tags.ts +++ b/packages/@overeng/utils/src/node/playwright/tags.ts @@ -12,7 +12,7 @@ import { Context, Layer } from 'effect' * * Provide this in Playwright tests to avoid threading `page` through every helper call. */ -export class PwPage extends Context.Tag('PwPage')() { +export class PwPage extends Context.Service()('PwPage') { static layer = (page: Page) => Layer.succeed(PwPage, page) } @@ -21,10 +21,9 @@ export class PwPage extends Context.Tag('PwPage')() { * * Provide this in Playwright tests to avoid threading `context` through every helper call. */ -export class PwBrowserContext extends Context.Tag('PwBrowserContext')< - PwBrowserContext, - BrowserContext ->() { +export class PwBrowserContext extends Context.Service()( + 'PwBrowserContext', +) { static layer = (context: BrowserContext) => Layer.succeed(PwBrowserContext, context) } diff --git a/packages/@overeng/utils/src/node/playwright/test.ts b/packages/@overeng/utils/src/node/playwright/test.ts index 9123f32528..965eeb1012 100644 --- a/packages/@overeng/utils/src/node/playwright/test.ts +++ b/packages/@overeng/utils/src/node/playwright/test.ts @@ -7,11 +7,11 @@ * @module */ -import { FetchHttpClient } from '@effect/platform' -import { NodeContext } from '@effect/platform-node' +import { NodeServices } from '@effect/platform-node' import type { BrowserContext, Page } from '@playwright/test' import { test } from '@playwright/test' import { ConfigProvider, Effect, Layer, Logger, LogLevel, type Config } from 'effect' +import { FetchHttpClient } from 'effect/unstable/http' import { OtelPlaywrightLive } from './otel.ts' import { PwBrowserContext, PwPage } from './tags.ts' @@ -76,7 +76,7 @@ export interface WithTestCtxParams { /** * Platform layers for filesystem, path, and HTTP client access. */ -const DefaultLayers = Layer.mergeAll(NodeContext.layer, FetchHttpClient.layer, TestEnvConfigLive) +const DefaultLayers = Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer, TestEnvConfigLive) /** Default timeout for Playwright tests (2 minutes). */ const DEFAULT_TIMEOUT_MS = 120_000 @@ -116,7 +116,7 @@ export const makeWithTestCtx = * Provides default layers: * - `PwPage` + `PwBrowserContext` (from Playwright fixtures) * - `OtelPlaywrightLive` (tracing, when OTEL endpoint is configured) - * - `NodeContext.layer` (filesystem, path, etc.) + * - `NodeServices.layer` (filesystem, path, etc.) * - `FetchHttpClient.layer` * - `Logger.minimumLogLevel(LogLevel.Debug)` (debug logs enabled by default) * @@ -190,7 +190,7 @@ const runWithTestCtx = ( ? usePrettyLogger === true ? Layer.mergeAll( Logger.minimumLogLevel(LogLevel.Debug), - Logger.replace(Logger.defaultLogger, Logger.prettyLogger()), + Logger.layer([Logger.consolePretty()]), ) : Logger.minimumLogLevel(LogLevel.Debug) : Layer.empty diff --git a/packages/@overeng/utils/src/node/playwright/wait.ts b/packages/@overeng/utils/src/node/playwright/wait.ts index 343154dd33..707c23dbe5 100644 --- a/packages/@overeng/utils/src/node/playwright/wait.ts +++ b/packages/@overeng/utils/src/node/playwright/wait.ts @@ -20,7 +20,7 @@ import { import { PwWaitAttemptAttrs, PwWaitOperation as PwWaitContract } from './pw.contract.ts' /** Error thrown when a polling wait operation times out */ -export class PwWaitTimeoutError extends Schema.TaggedError()( +export class PwWaitTimeoutError extends Schema.TaggedErrorClass()( 'PwWaitTimeoutError', { label: Schema.String, diff --git a/packages/@overeng/utils/src/node/workspace.ts b/packages/@overeng/utils/src/node/workspace.ts index e17e443340..2804502a7f 100644 --- a/packages/@overeng/utils/src/node/workspace.ts +++ b/packages/@overeng/utils/src/node/workspace.ts @@ -8,10 +8,10 @@ import { shouldNeverHappen } from '../isomorphic/mod.ts' export type WorkspaceInfo = string /** Current working directory. */ -export class CurrentWorkingDirectory extends Context.Tag('CurrentWorkingDirectory')< +export class CurrentWorkingDirectory extends Context.Service< CurrentWorkingDirectory, WorkspaceInfo ->() { +>()('CurrentWorkingDirectory') { /** Layer that captures the process cwd once. */ static live = Layer.effect( CurrentWorkingDirectory, @@ -23,10 +23,9 @@ export class CurrentWorkingDirectory extends Context.Tag('CurrentWorkingDirector } /** Workspace root (env required). */ -export class EffectUtilsWorkspace extends Context.Tag('EffectUtilsWorkspace')< - EffectUtilsWorkspace, - WorkspaceInfo ->() { +export class EffectUtilsWorkspace extends Context.Service()( + 'EffectUtilsWorkspace', +) { /** Resolve from WORKSPACE_ROOT env. */ static live = Layer.effect( EffectUtilsWorkspace, diff --git a/pnpm-install-contract.json b/pnpm-install-contract.json index 2169ec1d7b..2f9a8248a4 100644 --- a/pnpm-install-contract.json +++ b/pnpm-install-contract.json @@ -26,7 +26,7 @@ "installPolicy": { "dedupePeerDependents": true, "ignoreScripts": true, - "minimumReleaseAgeExclude": ["@effect/platform", "@types/node", "effect"], + "minimumReleaseAgeExclude": ["@effect/platform-node", "@types/node", "effect"], "optimisticRepeatInstall": false, "packageImportMethod": { "live": { @@ -40,9 +40,6 @@ }, "peerDependencyRules": { "allowedVersions": { - "@effect/experimental": ">=0.58.0", - "@effect/platform": ">=0.94.2", - "@effect/rpc": ">=0.73.0", "eslint": ">=10.0.0", "typescript": ">=6.0.0", "vitest": ">=4.0.0" @@ -142,8 +139,8 @@ "packages/@overeng/utils-dev" ], "patchedDependencies": { - "@effect/platform@0.96.2": "packages/@overeng/utils/patches/@effect__platform@0.96.0.patch", - "@myobie/pty@0.10.0": "packages/@overeng/utils/patches/@myobie__pty@0.10.0.patch" + "@myobie/pty@0.10.0": "packages/@overeng/utils/patches/@myobie__pty@0.10.0.patch", + "effect@4.0.0-beta.102": "packages/@overeng/utils/patches/effect@4.0.0-beta.102.patch" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e24671c210..cb433ec59c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,8 +8,8 @@ settings: packageExtensionsChecksum: sha256-4HimX0luQTvo3jbLtYHagQKuv0eSu+BxzCFfrE1bdnM= patchedDependencies: - '@effect/platform@0.96.2': 08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32 '@myobie/pty@0.10.0': e6d9b2f6990efbb98235e6e4018e3e9bb5ac94bc54ea6764364857206b6b95a5 + effect@4.0.0-beta.102: 0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f importers: @@ -17,18 +17,12 @@ importers: context/effect/socket: dependencies: - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) devDependencies: '@types/node': specifier: 26.0.0 @@ -36,12 +30,9 @@ importers: context/opentui: dependencies: - '@effect-atom/atom': - specifier: 0.5.3 - version: 0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect-atom/atom-react': - specifier: 0.5.0 - version: 0.5.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)(react@19.2.7)(scheduler@0.27.0) + '@effect/atom-react': + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0) '@opentui/core': specifier: 0.4.1 version: 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) @@ -49,8 +40,8 @@ importers: specifier: 0.4.1 version: 0.4.1(react-devtools-core@7.0.1)(react@19.2.7)(typescript@6.0.3)(web-tree-sitter@0.25.10)(ws@8.21.0) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -64,24 +55,21 @@ importers: packages/@overeng/agent-session-ingest: devDependencies: - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -91,21 +79,9 @@ importers: packages/@overeng/ci-tools: dependencies: - '@effect/cluster': - specifier: ^0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: ^0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) '@effect/opentelemetry': - specifier: ^0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/rpc': - specifier: ^0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/workflow': - specifier: ^0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: ^4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@overeng/otel-contract': specifier: workspace:^ version: link:../otel-contract @@ -115,22 +91,13 @@ importers: '@playwright/test': specifier: ^1.61.0 version: 1.61.0 - effect: - specifier: ^3.21.4 - version: 3.21.4 devDependencies: - '@effect/cli': - specifier: 0.75.2 - version: 0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ version: link:../utils-dev @@ -140,6 +107,9 @@ importers: '@types/node': specifier: 26.0.0 version: 26.0.0 + effect: + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -153,15 +123,15 @@ importers: specifier: 2.2.0 version: 2.2.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) devDependencies: '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 @@ -174,21 +144,15 @@ importers: packages/@overeng/effect-ai-claude-cli: devDependencies: - '@effect/ai': - specifier: 0.36.0 - version: 0.36.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -202,17 +166,17 @@ importers: packages/@overeng/effect-distributed-lock: devDependencies: '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -222,24 +186,21 @@ importers: packages/@overeng/effect-path: devDependencies: - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -255,7 +216,7 @@ importers: devDependencies: '@overeng/utils': specifier: workspace:^ - version: link:../utils + version: file:packages/@overeng/utils(@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)))(@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1))(@playwright/test@1.61.0)(@types/node@26.0.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) '@storybook/react': specifier: 10.4.6 version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)))(typescript@6.0.3) @@ -272,8 +233,8 @@ importers: specifier: 6.0.2 version: 6.0.2(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -298,24 +259,12 @@ importers: packages/@overeng/effect-rpc-tanstack: devDependencies: - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/sql': - specifier: 0.51.1 - version: 0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@overeng/utils': specifier: workspace:^ - version: link:../utils + version: file:packages/@overeng/utils(@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)))(@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1))(@playwright/test@1.61.0)(@types/node@26.0.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) '@tanstack/react-router': specifier: 1.170.16 version: 1.170.16(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -326,8 +275,8 @@ importers: specifier: 19.2.17 version: 19.2.17 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -346,12 +295,6 @@ importers: packages/@overeng/effect-rpc-tanstack/examples/basic: dependencies: - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) '@tanstack/react-router': specifier: 1.170.16 version: 1.170.16(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -359,8 +302,8 @@ importers: specifier: 1.168.26 version: 1.168.26(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -370,7 +313,7 @@ importers: devDependencies: '@overeng/utils': specifier: workspace:^ - version: link:../../../utils + version: file:packages/@overeng/utils(@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)))(@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1))(@playwright/test@1.61.0)(@types/node@26.0.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) '@playwright/test': specifier: 1.61.0 version: 1.61.0 @@ -405,8 +348,8 @@ importers: specifier: 19.2.3 version: 19.2.3(@types/react@19.2.17) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -428,7 +371,7 @@ importers: devDependencies: '@overeng/utils': specifier: workspace:^ - version: link:../utils + version: file:packages/@overeng/utils(@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)))(@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1))(@playwright/test@1.61.0)(@types/node@26.0.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) '@storybook/react': specifier: 10.4.6 version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)))(typescript@6.0.3) @@ -448,8 +391,8 @@ importers: specifier: 6.0.2 version: 6.0.2(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -477,39 +420,18 @@ importers: packages/@overeng/genie: dependencies: - '@effect-atom/atom': - specifier: 0.5.3 - version: 0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect-atom/atom-react': - specifier: 0.5.0 - version: 0.5.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)(react@19.2.7)(scheduler@0.27.0) - '@effect/cli': - specifier: 0.75.2 - version: 0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + '@effect/atom-react': + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0) '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@opentui/core': specifier: 0.4.1 version: 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) @@ -532,8 +454,8 @@ importers: specifier: 0.33.0 version: 0.33.0(@types/react@19.2.17) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -553,24 +475,18 @@ importers: '@overeng/tui-react': injected: true devDependencies: - '@effect/printer': - specifier: 0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4) - '@effect/printer-ansi': - specifier: 0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4) '@overeng/tui-core': specifier: workspace:^ version: link:../tui-core '@overeng/tui-react': specifier: workspace:^ - version: link:../tui-react + version: file:packages/@overeng/tui-react(3be8b8040347bbbbcb942f74e7efaa77) '@overeng/utils': specifier: workspace:^ - version: link:../utils + version: file:packages/@overeng/utils(@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)))(@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1))(@playwright/test@1.61.0)(@types/node@26.0.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@storybook/react-vite': specifier: 10.4.6 version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.27.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) @@ -590,17 +506,17 @@ importers: packages/@overeng/kdl: devDependencies: '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -615,17 +531,17 @@ importers: version: link:../kdl devDependencies: '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -635,39 +551,18 @@ importers: packages/@overeng/megarepo: dependencies: - '@effect-atom/atom': - specifier: 0.5.3 - version: 0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect-atom/atom-react': - specifier: 0.5.0 - version: 0.5.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)(react@19.2.7)(scheduler@0.27.0) - '@effect/cli': - specifier: 0.75.2 - version: 0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + '@effect/atom-react': + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0) '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@opentui/core': specifier: 0.4.1 version: 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) @@ -705,8 +600,8 @@ importers: specifier: 0.33.0 version: 0.33.0(@types/react@19.2.17) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -723,12 +618,6 @@ importers: '@overeng/tui-react': injected: true devDependencies: - '@effect/printer': - specifier: 0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4) - '@effect/printer-ansi': - specifier: 0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4) '@overeng/tui-core': specifier: workspace:^ version: link:../tui-core @@ -765,39 +654,18 @@ importers: packages/@overeng/notion-cli: dependencies: - '@effect-atom/atom': - specifier: 0.5.3 - version: 0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect-atom/atom-react': - specifier: 0.5.0 - version: 0.5.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)(react@19.2.7)(scheduler@0.27.0) - '@effect/cli': - specifier: 0.75.2 - version: 0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + '@effect/atom-react': + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0) '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@opentui/core': specifier: 0.4.1 version: 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) @@ -844,8 +712,8 @@ importers: specifier: 0.33.0 version: 0.33.0(@types/react@19.2.17) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -862,12 +730,6 @@ importers: '@overeng/tui-react': injected: true devDependencies: - '@effect/sql': - specifier: 0.51.1 - version: 0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/typeclass': - specifier: 0.40.0 - version: 0.40.0(effect@3.21.4) '@overeng/utils-dev': specifier: workspace:^ version: link:../utils-dev @@ -894,13 +756,13 @@ importers: devDependencies: '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -944,39 +806,18 @@ importers: '@overeng/tui-react': injected: true devDependencies: - '@effect-atom/atom': - specifier: 0.5.3 - version: 0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect-atom/atom-react': - specifier: 0.5.0 - version: 0.5.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)(react@19.2.7)(scheduler@0.27.0) - '@effect/cli': - specifier: 0.75.2 - version: 0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + '@effect/atom-react': + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0) '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@opentui/core': specifier: 0.4.1 version: 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) @@ -999,8 +840,8 @@ importers: specifier: 0.33.0 version: 0.33.0(@types/react@19.2.17) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react-dom: specifier: 19.2.7 version: 19.2.7(react@19.2.7) @@ -1016,30 +857,15 @@ importers: packages/@overeng/notion-effect-client: dependencies: - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/content-address': specifier: workspace:^ version: link:../content-address @@ -1059,8 +885,8 @@ importers: specifier: 1.61.0 version: 1.61.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) mdast-util-gfm-strikethrough: specifier: 2.0.0 version: 2.0.0 @@ -1112,17 +938,17 @@ importers: version: link:../notion-core devDependencies: '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -1154,39 +980,18 @@ importers: specifier: workspace:^ version: link:../utils devDependencies: - '@effect-atom/atom': - specifier: 0.5.3 - version: 0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/cli': - specifier: 0.75.2 - version: 0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/tui-react': specifier: workspace:^ - version: link:../tui-react + version: file:packages/@overeng/tui-react(d3c0a82ae4c3820b85cb46e13b611cfc) '@overeng/utils-dev': specifier: workspace:^ version: link:../utils-dev @@ -1212,8 +1017,8 @@ importers: specifier: 6.0.2 version: 6.0.2(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -1243,17 +1048,17 @@ importers: version: link:../notion-effect-schema devDependencies: '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -1263,30 +1068,15 @@ importers: packages/@overeng/notion-react: dependencies: - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/notion-effect-client': specifier: workspace:^ version: link:../notion-effect-client @@ -1300,8 +1090,8 @@ importers: specifier: 1.61.0 version: 1.61.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) vitest: specifier: 4.1.9 version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) @@ -1363,21 +1153,15 @@ importers: packages/@overeng/npm-release: devDependencies: - '@effect/cli': - specifier: 0.75.2 - version: 0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -1393,13 +1177,13 @@ importers: devDependencies: '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -1444,17 +1228,17 @@ importers: version: link:../otel-contract devDependencies: '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/utils-dev': specifier: workspace:^ - version: link:../utils-dev + version: file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -1493,8 +1277,8 @@ importers: specifier: 6.0.2 version: 6.0.2(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) effect: - specifier: 4.0.0-beta.99 - version: 4.0.0-beta.99 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) happy-dom: specifier: 20.10.6 version: 20.10.6 @@ -1519,24 +1303,9 @@ importers: packages/@overeng/restate-effect: dependencies: - '@effect/cluster': - specifier: ^0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: ^0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/platform': - specifier: ^0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) '@effect/platform-node': - specifier: ^0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: ^0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/workflow': - specifier: ^0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: ^4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@overeng/otel-contract': specifier: workspace:^ version: link:../otel-contract @@ -1551,11 +1320,11 @@ importers: version: 1.14.5 devDependencies: '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@opentelemetry/api': specifier: 1.9.1 version: 1.9.1 @@ -1593,8 +1362,8 @@ importers: specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -1616,30 +1385,15 @@ importers: packages/@overeng/tui-react: dependencies: - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@overeng/tui-core': specifier: workspace:^ version: link:../tui-core @@ -1665,8 +1419,8 @@ importers: specifier: 6.0.0 version: 6.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) string-width: specifier: 8.2.1 version: 8.2.1 @@ -1677,15 +1431,9 @@ importers: specifier: 3.2.1 version: 3.2.1 devDependencies: - '@effect-atom/atom': - specifier: 0.5.3 - version: 0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect-atom/atom-react': - specifier: 0.5.0 - version: 0.5.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)(react@19.2.7)(scheduler@0.27.0) - '@effect/cli': - specifier: 0.75.2 - version: 0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + '@effect/atom-react': + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0) '@opentui/core': specifier: 0.4.1 version: 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) @@ -1734,45 +1482,18 @@ importers: packages/@overeng/tui-stories: dependencies: - '@effect-atom/atom': - specifier: 0.5.3 - version: 0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect-atom/atom-react': - specifier: 0.5.0 - version: 0.5.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)(react@19.2.7)(scheduler@0.27.0) - '@effect/cli': - specifier: 0.75.2 - version: 0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + '@effect/atom-react': + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0) '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/printer': - specifier: 0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4) - '@effect/printer-ansi': - specifier: 0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@opentui/core': specifier: 0.4.1 version: 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) @@ -1801,8 +1522,8 @@ importers: specifier: 0.33.0 version: 0.33.0(@types/react@19.2.17) effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: specifier: 19.2.7 version: 19.2.7 @@ -1841,17 +1562,14 @@ importers: packages/@overeng/utils: dependencies: '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@noble/hashes': specifier: 2.2.0 version: 2.2.0 @@ -1865,24 +1583,12 @@ importers: specifier: workspace:^ version: link:../otel-contract effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) vitest: specifier: 4.1.9 version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) devDependencies: - '@effect/cluster': - specifier: 0.59.0 - version: 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/experimental': - specifier: 0.60.0 - version: 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': - specifier: 0.75.1 - version: 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/workflow': - specifier: 0.18.2 - version: 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) '@opentelemetry/resources': specifier: 2.8.0 version: 2.8.0(@opentelemetry/api@1.9.1) @@ -1929,23 +1635,20 @@ importers: packages/@overeng/utils-dev: devDependencies: '@effect/opentelemetry': - specifier: 0.63.0 - version: 0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4) - '@effect/platform': - specifier: 0.96.2 - version: 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) '@effect/platform-node': - specifier: 0.107.0 - version: 0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) '@effect/vitest': - specifier: 0.29.0 - version: 0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) '@types/node': specifier: 26.0.0 version: 26.0.0 effect: - specifier: 3.21.4 - version: 3.21.4 + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) typescript: specifier: 6.0.3 version: 6.0.3 @@ -1966,6 +1669,10 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} @@ -2004,6 +1711,10 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -2021,6 +1732,10 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -2033,64 +1748,19 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@effect-atom/atom-react@0.5.0': - resolution: {integrity: sha512-aFfjWi4rEJCqfM12Oi36/EKaDm/W6n4/N6yM5vL0t/QozKhJhK05rQL/GY4XMxlH2eqkQ4ih8jBQa3Yyp0Fiqw==} + '@effect/atom-react@4.0.0-beta.102': + resolution: {integrity: sha512-6SX11h7OFwje4YhxYUml9qGjBU8hvnEMrLWe3TKI3lEH/W6tkU/BzajC263eXkxM68h+BJNzE81Ipi5DTfEgug==} peerDependencies: - effect: ^3.19 - react: '>=18 <20' + effect: ^4.0.0-beta.102 + react: ^19.2.4 scheduler: '*' - '@effect-atom/atom@0.5.3': - resolution: {integrity: sha512-TRZv/i+YT3TtnN0oFORJqXdxSs1fc7lrJlH+1xZvDFyjC9hgoVnrcKbeZsDFmr6r0wYRqVo7U3IftxiQNjpNZA==} - peerDependencies: - '@effect/experimental': ^0.58.0 - '@effect/platform': ^0.94.2 - '@effect/rpc': ^0.73.0 - effect: ^3.19.15 - - '@effect/ai@0.36.0': - resolution: {integrity: sha512-GDYSedxjEAvEovZr6x+ry1n1Mp0mWBLoMzPJGxxnLC3qjyLsIJ89Cvfr4+YijQ6et0osusHxeULzrFFSnLYJJA==} - peerDependencies: - '@effect/experimental': ^0.60.0 - '@effect/platform': ^0.96.1 - '@effect/rpc': ^0.75.1 - effect: ^3.21.3 - - '@effect/cli@0.75.2': - resolution: {integrity: sha512-+cMn/ZtFB+jEvgB6phrTT6XyPkIUnmX4G0nUSln7yj3lwyiHQ578Iwr8nHeU1CKEtlh4Xt+mYc0yiT912xu7YA==} - peerDependencies: - '@effect/platform': ^0.96.1 - '@effect/printer': ^0.49.0 - '@effect/printer-ansi': ^0.49.0 - effect: ^3.21.2 - - '@effect/cluster@0.59.0': - resolution: {integrity: sha512-rxtJ0zlcdDDtn9wau0z/PFfwPP2FsYPB2/tSK4tT8DkTYQmACJ5AwRwZm3Ea83iL/gv3df63lRe94oMiS8bFfg==} - peerDependencies: - '@effect/platform': ^0.96.1 - '@effect/rpc': ^0.75.1 - '@effect/sql': ^0.51.1 - '@effect/workflow': ^0.18.2 - effect: ^3.21.2 - - '@effect/experimental@0.60.0': - resolution: {integrity: sha512-i5zIg7Xup2KgHyqHlYtkgqSE1bNzCL0GbbTQxrpIzKF0q/ebknOk/ox8B/gIq2vImjoEE81h/oxU+6i1NH210g==} + '@effect/opentelemetry@4.0.0-beta.102': + resolution: {integrity: sha512-U15cxJ1uf3+HMnZB0BUQe3U4TuyIPYxSHJ+wEzkF4ZjteN224hsgeTgsyXSFjSIpWDVWfujdGPqmtkDpcMzf6w==} + engines: {node: '>=18.0.0'} peerDependencies: - '@effect/platform': ^0.96.0 - effect: ^3.21.0 - ioredis: ^5 - lmdb: ^3 - peerDependenciesMeta: - ioredis: - optional: true - lmdb: - optional: true - - '@effect/opentelemetry@0.63.0': - resolution: {integrity: sha512-2yUG2QWNATi1uKP0kwhaP5eLp+c5NDzAL3EOpIcGLBAC0cbXZrx4n9Qw/QwUKxpuV+pbhrBUPCiByyWAFKfuCw==} - peerDependencies: - '@effect/platform': ^0.96.0 '@opentelemetry/api': ^1.9 + '@opentelemetry/api-logs': '>=0.203.0 <0.300.0' '@opentelemetry/resources': ^2.0.0 '@opentelemetry/sdk-logs': '>=0.203.0 <0.300.0' '@opentelemetry/sdk-metrics': ^2.0.0 @@ -2098,74 +1768,43 @@ packages: '@opentelemetry/sdk-trace-node': ^2.0.0 '@opentelemetry/sdk-trace-web': ^2.0.0 '@opentelemetry/semantic-conventions': ^1.33.0 - effect: ^3.21.0 - - '@effect/platform-node-shared@0.60.0': - resolution: {integrity: sha512-obT8Mau/guSYjehF9pg09FHM/ERXAAVBl/FXRXEyvTc7ZiDGTYnPBihytGNccsfNsgqBdvCE24sflwK6j5nFXA==} - peerDependencies: - '@effect/cluster': ^0.59.0 - '@effect/platform': ^0.96.1 - '@effect/rpc': ^0.75.1 - '@effect/sql': ^0.51.1 - effect: ^3.21.2 - - '@effect/platform-node@0.107.0': - resolution: {integrity: sha512-VWUD9SCcnsrXYqED2jIwDHHDGb3QePNsHlX7HfqQGD2/AgnfX9jNnecaFrJj7Rf2gK5VbRE+i/2oNFoC/DkhcA==} - peerDependencies: - '@effect/cluster': ^0.59.0 - '@effect/platform': ^0.96.1 - '@effect/rpc': ^0.75.1 - '@effect/sql': ^0.51.1 - effect: ^3.21.2 - - '@effect/platform@0.96.2': - resolution: {integrity: sha512-oJm3UztdzZvK7BXkFSV3IdyGuqQrhLmViG/hMDZh99ski7aADesNuv19z4R0heH3bAXnSt/Nb8O9KJQ886C0Tg==} - peerDependencies: - effect: ^3.21.4 - - '@effect/printer-ansi@0.49.0': - resolution: {integrity: sha512-N2OyqDTqcGLKeUy2URowThoU5issZQwG/Ihv5qOYWJD0neq9qBIgC57/9BkFpTRPNSMtPHyCOk1TFj297HGLLQ==} - peerDependencies: - '@effect/typeclass': ^0.40.0 - effect: ^3.21.0 - - '@effect/printer@0.49.0': - resolution: {integrity: sha512-hrjTuExF87wuWjOnnND1c2fKcCWhleQBVaoA7JlrU3rC7s+RYPETDOXtpgAK3/uuMCRnDhfVFQMevtKT8MBdKg==} - peerDependencies: - '@effect/typeclass': ^0.40.0 - effect: ^3.21.0 - - '@effect/rpc@0.75.1': - resolution: {integrity: sha512-8yxF8+mMGGEbF8BUCp34HjdJj7CvTpGeZxBcpsDF6v7zPiGbJL1UDLzA8ZqYjmcngBHhPecbmeONTk/LiLAaEg==} - peerDependencies: - '@effect/platform': ^0.96.1 - effect: ^3.21.2 - - '@effect/sql@0.51.1': - resolution: {integrity: sha512-iPDAefrJcI0HcTk9keP9Gq8Pg08K1HmpnmZZt85AqyTcvorhoNsXDFiKBbPldfV2CortwVkacX8KjO9GPpSYCA==} - peerDependencies: - '@effect/experimental': ^0.60.0 - '@effect/platform': ^0.96.1 - effect: ^3.21.2 + effect: ^4.0.0-beta.102 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/api-logs': + optional: true + '@opentelemetry/resources': + optional: true + '@opentelemetry/sdk-logs': + optional: true + '@opentelemetry/sdk-metrics': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + '@opentelemetry/sdk-trace-node': + optional: true + '@opentelemetry/sdk-trace-web': + optional: true - '@effect/typeclass@0.40.0': - resolution: {integrity: sha512-L/2o2ImeqbemFlqH0b3y2PqQTFc+E0/DUnffCU8bkJUGh0yUZmh2RXuXhR8QOpfNCe718JQjI+mLnpVF2MMmaQ==} + '@effect/platform-node-shared@4.0.0-beta.102': + resolution: {integrity: sha512-gVd793I72MrkX4dXo7eYtRKfNj0RW4eMRfVEKEJI16h2+mBDCzQ+gqMrog2hSTHQnaIbvbYShNQ4TVGuRCYZeQ==} + engines: {node: '>=18.0.0'} peerDependencies: - effect: ^3.21.0 + effect: ^4.0.0-beta.102 - '@effect/vitest@0.29.0': - resolution: {integrity: sha512-DvWr1aeEcaZ8mtu8hNVb4e3rEYvGEwQSr7wsNrW53t6nKYjkmjRICcvVEsXUhjoCblRHSxRsRV0TOt0+UmcvaQ==} + '@effect/platform-node@4.0.0-beta.102': + resolution: {integrity: sha512-wYVAU9jAePT+gouMr/EVz1CaW6yDLjPAgXYeEFLz7wugT5iZhFRag6mqajV/wwN3bzWP2fzZHokLuPmqD/rqeA==} + engines: {node: '>=18.0.0'} peerDependencies: - effect: ^3.21.0 - vitest: ^3.2.0 + effect: ^4.0.0-beta.102 + ioredis: ^5.7.0 - '@effect/workflow@0.18.2': - resolution: {integrity: sha512-2av0TexhxHnapQa3eLn6TkniokUFRZKf8dlWlLqbODMpXAJ3mm+DOHWM0ozMGkg9K/+zGCtiL3dMqJyJ8r7G8g==} + '@effect/vitest@4.0.0-beta.102': + resolution: {integrity: sha512-4dipFAYG6imOzrY3zy3BgzCJkbb9xESyzUef0WSx8bsK0/SpITqbJpdwKeOyDWLZ+rimjua8IbTFMAde865pIQ==} peerDependencies: - '@effect/experimental': ^0.60.0 - '@effect/platform': ^0.96.1 - '@effect/rpc': ^0.75.1 - effect: ^3.21.2 + effect: ^4.0.0-beta.102 + vitest: ^3.0.0 || ^4.0.0 '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -2406,6 +2045,9 @@ packages: '@internationalized/string@3.2.9': resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==} + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} peerDependencies: @@ -2431,61 +2073,31 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} - cpu: [arm64] - os: [darwin] - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} cpu: [arm64] os: [darwin] - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} - cpu: [x64] - os: [darwin] - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} cpu: [x64] os: [darwin] - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} - cpu: [arm64] - os: [linux] - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} cpu: [arm64] os: [linux] - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} - cpu: [arm] - os: [linux] - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} cpu: [arm] os: [linux] - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} - cpu: [x64] - os: [linux] - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} cpu: [x64] os: [linux] - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} - cpu: [x64] - os: [win32] - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} cpu: [x64] @@ -2501,6 +2113,12 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@noble/hashes@2.2.0': resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} engines: {node: '>= 20.19.0'} @@ -2581,6 +2199,10 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + '@opentui/core-darwin-arm64@0.4.1': resolution: {integrity: sha512-ocs73hj9n0zLArOTpUWIXCWU6ERThG+3wQzO78EvfaR4hb5FRrDHGKWTzXpr6ukSKsUtKdztK5XYTPsJ5e3vww==} cpu: [arm64] @@ -2635,6 +2257,54 @@ packages: react-devtools-core: ^7.0.1 ws: ^8.18.0 + '@overeng/content-address@file:packages/@overeng/content-address': + resolution: {directory: packages/@overeng/content-address, type: directory} + + '@overeng/effect-distributed-lock@file:packages/@overeng/effect-distributed-lock': + resolution: {directory: packages/@overeng/effect-distributed-lock, type: directory} + peerDependencies: + effect: ^4.0.0-beta.102 + + '@overeng/otel-contract@file:packages/@overeng/otel-contract': + resolution: {directory: packages/@overeng/otel-contract, type: directory} + peerDependencies: + effect: ^4.0.0-beta.102 + + '@overeng/tui-core@file:packages/@overeng/tui-core': + resolution: {directory: packages/@overeng/tui-core, type: directory} + + '@overeng/tui-react@file:packages/@overeng/tui-react': + resolution: {directory: packages/@overeng/tui-react, type: directory} + peerDependencies: + '@effect/atom-react': ^4.0.0-beta.102 + '@effect/platform-node': ^4.0.0-beta.102 + '@opentui/core': ^0.4.1 + '@opentui/react': ^0.4.1 + '@storybook/react': ^10.4.6 + '@types/react': ^19.2.17 + '@types/react-reconciler': ^0.33.0 + effect: ^4.0.0-beta.102 + react: ^19.2.7 + react-dom: ^19.2.7 + react-reconciler: ^0.33.0 + + '@overeng/utils-dev@file:packages/@overeng/utils-dev': + resolution: {directory: packages/@overeng/utils-dev, type: directory} + peerDependencies: + '@effect/opentelemetry': ^4.0.0-beta.102 + '@effect/platform-node': ^4.0.0-beta.102 + '@effect/vitest': ^4.0.0-beta.102 + effect: ^4.0.0-beta.102 + vitest: ^4.1.9 + + '@overeng/utils@file:packages/@overeng/utils': + resolution: {directory: packages/@overeng/utils, type: directory} + peerDependencies: + '@effect/opentelemetry': ^4.0.0-beta.102 + '@effect/platform-node': ^4.0.0-beta.102 + '@playwright/test': ^1.61.0 + effect: ^4.0.0-beta.102 + '@oxc-parser/binding-android-arm-eabi@0.127.0': resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2901,94 +2571,6 @@ packages: cpu: [x64] os: [win32] - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} - engines: {node: '>= 10.0.0'} - '@playwright/test@1.61.0': resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==} engines: {node: '>=18'} @@ -3493,6 +3075,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -3842,6 +3427,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -3896,6 +3485,10 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -3925,11 +3518,8 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - effect@3.21.4: - resolution: {integrity: sha512-B89v/xSgPbl1J2Ai2u18jxq3odpFauU1rC6/eSs4FeNHi72kwKdJp12VGigvRV2lK+kRnx+OOz41XV8guZd4gQ==} - - effect@4.0.0-beta.99: - resolution: {integrity: sha512-hP1C61uzINfLl/4kKMwcqksxd34s4sQ3VSjsWjhGrkx9CRlXaqnfOK9dpTEKynQ6rA7wU9rb3c+48eDYw7uzxA==} + effect@4.0.0-beta.102: + resolution: {integrity: sha512-z8Y+Q76Hh/kjLFZrXu8tGn6e+tDsg45R+UHhxd190pXxD53OGwf/G/zDxXTkse4HJ5mobNZfitLfUCp4fMvu6w==} electron-to-chromium@1.5.344: resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} @@ -4032,10 +3622,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fast-check@3.23.2: - resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} - engines: {node: '>=8.0.0'} - fast-check@4.9.0: resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} engines: {node: '>=12.17.0'} @@ -4150,14 +3736,14 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - ini@4.1.3: - resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ini@7.0.0: resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -4258,30 +3844,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -4289,6 +3905,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -4296,6 +3919,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -4303,6 +3933,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -4310,22 +3947,45 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -4457,9 +4117,9 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} + mime@4.1.0: + resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} + engines: {node: '>=16'} hasBin: true min-indent@1.0.1: @@ -4480,28 +4140,18 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} - hasBin: true - msgpackr-extract@3.0.4: resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true - msgpackr@1.11.10: - resolution: {integrity: sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==} - msgpackr@2.0.4: resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==} - multipasta@0.2.7: - resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} - multipasta@0.2.8: resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} - nanoid@3.3.13: - resolution: {integrity: sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -4587,6 +4237,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + playwright-core@1.61.0: resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==} engines: {node: '>=18'} @@ -4597,8 +4251,8 @@ packages: engines: {node: '>=18'} hasBin: true - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -4621,9 +4275,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - pure-rand@8.4.2: resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} @@ -4686,6 +4337,14 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -4787,9 +4446,17 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + srvx@0.11.22: + resolution: {integrity: sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==} + engines: {node: '>=20.16.0'} + hasBin: true + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} @@ -4876,9 +4543,6 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - toml@4.3.0: resolution: {integrity: sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==} engines: {node: '>=20'} @@ -4921,9 +4585,9 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - undici@7.25.0: - resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} - engines: {node: '>=20.18.1'} + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -4965,10 +4629,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - uuid@14.0.1: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true @@ -5100,8 +4760,8 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - ws@7.5.12: - resolution: {integrity: sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg==} + ws@7.5.13: + resolution: {integrity: sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==} engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 @@ -5124,6 +4784,18 @@ packages: utf-8-validate: optional: true + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -5135,11 +4807,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} - engines: {node: '>= 14.6'} - hasBin: true - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -5174,6 +4841,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.29.0': {} '@babel/core@7.29.0': @@ -5234,6 +4907,8 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.29.2': @@ -5247,6 +4922,8 @@ snapshots: '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -5270,146 +4947,65 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@effect-atom/atom-react@0.5.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)(react@19.2.7)(scheduler@0.27.0)': + '@effect/atom-react@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0)': dependencies: - '@effect-atom/atom': 0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - effect: 3.21.4 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) react: 19.2.7 scheduler: 0.27.0 - transitivePeerDependencies: - - '@effect/experimental' - - '@effect/platform' - - '@effect/rpc' - - '@effect-atom/atom@0.5.3(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)': - dependencies: - '@effect/experimental': 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - '@effect/rpc': 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - effect: 3.21.4 - - '@effect/ai@0.36.0(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)': - dependencies: - '@effect/experimental': 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - '@effect/rpc': 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - effect: 3.21.4 - find-my-way-ts: 0.1.6 - - '@effect/cli@0.75.2(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)': - dependencies: - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - '@effect/printer': 0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4) - '@effect/printer-ansi': 0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4) - effect: 3.21.4 - ini: 4.1.3 - toml: 3.0.0 - yaml: 2.8.3 - - '@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40)': - dependencies: - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - '@effect/rpc': 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/sql': 0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/workflow': 0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - effect: 3.21.4 - kubernetes-types: 1.30.0 - '@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4)': + '@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))': dependencies: - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - effect: 3.21.4 - uuid: 11.1.0 + '@opentelemetry/semantic-conventions': 1.41.1 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.219.0 + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.219.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-web': 2.8.0(@opentelemetry/api@1.9.1) - '@effect/opentelemetry@0.63.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@3.21.4)': + '@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))': dependencies: - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) + '@opentelemetry/semantic-conventions': 1.43.0 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + optionalDependencies: '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.219.0 '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-logs': 0.219.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': 2.8.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-node': 2.8.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-web': 2.8.0(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - effect: 3.21.4 - '@effect/platform-node-shared@0.60.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)': + '@effect/platform-node-shared@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))': dependencies: - '@effect/cluster': 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - '@effect/rpc': 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/sql': 0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@parcel/watcher': 2.5.6 - effect: 3.21.4 - multipasta: 0.2.7 - ws: 8.21.0 + '@types/ws': 8.18.1 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + ws: 8.21.1 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@0.107.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)': + '@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1)': dependencies: - '@effect/cluster': 0.59.0(2726862cc6f36e3b086be9ebab8bdc40) - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - '@effect/platform-node-shared': 0.60.0(@effect/cluster@0.59.0(2726862cc6f36e3b086be9ebab8bdc40))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4) - '@effect/rpc': 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/sql': 0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - effect: 3.21.4 - mime: 3.0.0 - undici: 7.25.0 - ws: 8.21.0 + '@effect/platform-node-shared': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + ioredis: 5.11.1 + mime: 4.1.0 + undici: 8.9.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4)': - dependencies: - effect: 3.21.4 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.10 - multipasta: 0.2.7 - - '@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4)': - dependencies: - '@effect/printer': 0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4) - '@effect/typeclass': 0.40.0(effect@3.21.4) - effect: 3.21.4 - - '@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.4))(effect@3.21.4)': - dependencies: - '@effect/typeclass': 0.40.0(effect@3.21.4) - effect: 3.21.4 - - '@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4)': - dependencies: - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - effect: 3.21.4 - msgpackr: 1.11.10 - - '@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4)': - dependencies: - '@effect/experimental': 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - effect: 3.21.4 - uuid: 11.1.0 - - '@effect/typeclass@0.40.0(effect@3.21.4)': - dependencies: - effect: 3.21.4 - - '@effect/vitest@0.29.0(effect@3.21.4)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)))': + '@effect/vitest@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)))': dependencies: - effect: 3.21.4 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) - '@effect/workflow@0.18.2(@effect/experimental@0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(@effect/rpc@0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4))(effect@3.21.4)': - dependencies: - '@effect/experimental': 0.60.0(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - '@effect/platform': 0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4) - '@effect/rpc': 0.75.1(@effect/platform@0.96.2(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.4))(effect@3.21.4) - effect: 3.21.4 - '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -5589,6 +5185,8 @@ snapshots: dependencies: '@swc/helpers': 0.5.21 + '@ioredis/commands@1.10.0': {} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))': dependencies: glob: 13.0.6 @@ -5616,39 +5214,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - optional: true - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - optional: true - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - optional: true - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - optional: true - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - optional: true - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': optional: true - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - optional: true - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': optional: true @@ -5660,13 +5240,6 @@ snapshots: node-pty: 1.1.0 smol-toml: 1.6.1 - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - optional: true - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': dependencies: '@emnapi/core': 1.11.0 @@ -5681,6 +5254,13 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + '@noble/hashes@2.2.0': {} '@oozcitak/dom@2.0.2': @@ -5757,6 +5337,8 @@ snapshots: '@opentelemetry/semantic-conventions@1.41.1': {} + '@opentelemetry/semantic-conventions@1.43.0': {} + '@opentui/core-darwin-arm64@0.4.1': optional: true @@ -5812,6 +5394,191 @@ snapshots: - typescript - web-tree-sitter + '@opentui/react@0.4.1(react-devtools-core@7.0.1)(react@19.2.7)(typescript@6.0.3)(web-tree-sitter@0.25.10)(ws@8.21.1)': + dependencies: + '@opentui/core': 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) + react: 19.2.7 + react-devtools-core: 7.0.1 + react-reconciler: 0.33.0(react@19.2.7) + ws: 8.21.1 + transitivePeerDependencies: + - typescript + - web-tree-sitter + + '@overeng/content-address@file:packages/@overeng/content-address': + dependencies: + '@noble/hashes': 2.2.0 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + + '@overeng/effect-distributed-lock@file:packages/@overeng/effect-distributed-lock(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))': + dependencies: + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + + '@overeng/otel-contract@file:packages/@overeng/otel-contract(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))': + dependencies: + '@overeng/content-address': file:packages/@overeng/content-address + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + + '@overeng/tui-core@file:packages/@overeng/tui-core': {} + + '@overeng/tui-react@file:packages/@overeng/tui-react(3be8b8040347bbbbcb942f74e7efaa77)': + dependencies: + '@effect/atom-react': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0) + '@effect/opentelemetry': 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + '@effect/platform-node': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) + '@effect/vitest': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + '@opentui/core': 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) + '@opentui/react': 0.4.1(react-devtools-core@7.0.1)(react@19.2.7)(typescript@6.0.3)(web-tree-sitter@0.25.10)(ws@8.21.0) + '@overeng/tui-core': file:packages/@overeng/tui-core + '@overeng/utils': file:packages/@overeng/utils(@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)))(@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1))(@playwright/test@1.61.0)(@types/node@26.0.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + '@playwright/test': 1.61.0 + '@storybook/react': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)))(typescript@6.0.3) + '@types/react': 19.2.17 + '@types/react-reconciler': 0.33.0(@types/react@19.2.17) + '@xterm/addon-fit': 0.11.0 + '@xterm/addon-webgl': 0.19.0 + '@xterm/headless': 6.0.0 + '@xterm/xterm': 6.0.0 + cli-truncate: 6.0.0 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-reconciler: 0.33.0(react@19.2.7) + string-width: 8.2.1 + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + yoga-layout: 3.2.1 + transitivePeerDependencies: + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@opentelemetry/api-logs' + - '@opentelemetry/resources' + - '@opentelemetry/sdk-logs' + - '@opentelemetry/sdk-metrics' + - '@opentelemetry/sdk-trace-base' + - '@opentelemetry/sdk-trace-node' + - '@opentelemetry/sdk-trace-web' + - '@opentelemetry/semantic-conventions' + - '@types/node' + - '@vitest/browser-playwright' + - '@vitest/browser-preview' + - '@vitest/browser-webdriverio' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - happy-dom + - jsdom + - msw + - vite + + '@overeng/tui-react@file:packages/@overeng/tui-react(d3c0a82ae4c3820b85cb46e13b611cfc)': + dependencies: + '@effect/atom-react': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(react@19.2.7)(scheduler@0.27.0) + '@effect/opentelemetry': 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + '@effect/platform-node': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) + '@effect/vitest': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + '@opentui/core': 0.4.1(typescript@6.0.3)(web-tree-sitter@0.25.10) + '@opentui/react': 0.4.1(react-devtools-core@7.0.1)(react@19.2.7)(typescript@6.0.3)(web-tree-sitter@0.25.10)(ws@8.21.1) + '@overeng/tui-core': file:packages/@overeng/tui-core + '@overeng/utils': file:packages/@overeng/utils(@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)))(@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1))(@playwright/test@1.61.0)(@types/node@26.0.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + '@playwright/test': 1.61.0 + '@storybook/react': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)))(typescript@6.0.3) + '@types/react': 19.2.17 + '@types/react-reconciler': 0.33.0(@types/react@19.2.17) + '@xterm/addon-fit': 0.11.0 + '@xterm/addon-webgl': 0.19.0 + '@xterm/headless': 6.0.0 + '@xterm/xterm': 6.0.0 + cli-truncate: 6.0.0 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-reconciler: 0.33.0(react@19.2.7) + string-width: 8.2.1 + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + yoga-layout: 3.2.1 + transitivePeerDependencies: + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@opentelemetry/api-logs' + - '@opentelemetry/resources' + - '@opentelemetry/sdk-logs' + - '@opentelemetry/sdk-metrics' + - '@opentelemetry/sdk-trace-base' + - '@opentelemetry/sdk-trace-node' + - '@opentelemetry/sdk-trace-web' + - '@opentelemetry/semantic-conventions' + - '@types/node' + - '@vitest/browser-playwright' + - '@vitest/browser-preview' + - '@vitest/browser-webdriverio' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - happy-dom + - jsdom + - msw + - vite + + '@overeng/utils-dev@file:packages/@overeng/utils-dev(fafa27166f9fea900351b0e7d1b455e5)': + dependencies: + '@effect/opentelemetry': 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + '@effect/platform-node': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) + '@effect/vitest': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + + ? '@overeng/utils@file:packages/@overeng/utils(@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)))(@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1))(@playwright/test@1.61.0)(@types/node@26.0.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))' + : dependencies: + '@effect/opentelemetry': 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + '@effect/platform-node': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) + '@effect/vitest': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + '@noble/hashes': 2.2.0 + '@opentelemetry/api': 1.9.1 + '@overeng/effect-distributed-lock': file:packages/@overeng/effect-distributed-lock(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + '@overeng/otel-contract': file:packages/@overeng/otel-contract(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + '@playwright/test': 1.61.0 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + transitivePeerDependencies: + - '@edge-runtime/vm' + - '@types/node' + - '@vitest/browser-playwright' + - '@vitest/browser-preview' + - '@vitest/browser-webdriverio' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - happy-dom + - jsdom + - msw + - vite + + ? '@overeng/utils@file:packages/@overeng/utils(@effect/opentelemetry@4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)))(@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1))(@playwright/test@1.61.0)(@types/node@26.0.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))' + : dependencies: + '@effect/opentelemetry': 4.0.0-beta.102(@opentelemetry/api-logs@0.219.0)(@opentelemetry/api@1.9.1)(@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-web@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.43.0)(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + '@effect/platform-node': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(ioredis@5.11.1) + '@effect/vitest': 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f))(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))) + '@noble/hashes': 2.2.0 + '@opentelemetry/api': 1.9.1 + '@overeng/effect-distributed-lock': file:packages/@overeng/effect-distributed-lock(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + '@overeng/otel-contract': file:packages/@overeng/otel-contract(effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f)) + '@playwright/test': 1.61.0 + effect: 4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f) + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(happy-dom@20.10.6)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + transitivePeerDependencies: + - '@edge-runtime/vm' + - '@types/node' + - '@vitest/browser-playwright' + - '@vitest/browser-preview' + - '@vitest/browser-webdriverio' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - happy-dom + - jsdom + - msw + - vite + '@oxc-parser/binding-android-arm-eabi@0.127.0': optional: true @@ -5959,66 +5726,6 @@ snapshots: '@oxlint-tsgolint/win32-x64@0.23.0': optional: true - '@parcel/watcher-android-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-x64@2.5.6': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.6': - optional: true - - '@parcel/watcher-win32-arm64@2.5.6': - optional: true - - '@parcel/watcher-win32-ia32@2.5.6': - optional: true - - '@parcel/watcher-win32-x64@2.5.6': - optional: true - - '@parcel/watcher@2.5.6': - dependencies: - detect-libc: 2.1.2 - is-glob: 4.0.3 - node-addon-api: 7.1.1 - picomatch: 4.0.4 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 - '@playwright/test@1.61.0': dependencies: playwright: 1.61.0 @@ -6085,7 +5792,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@rolldown/binding-win32-arm64-msvc@1.0.3': @@ -6483,8 +6190,8 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.29.2 + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -6520,6 +6227,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -6889,6 +6601,8 @@ snapshots: clsx@2.1.1: {} + cluster-key-slot@1.1.1: {} + comma-separated-tokens@2.0.3: {} commander@8.3.0: {} @@ -6928,6 +6642,8 @@ snapshots: define-lazy-prop@3.0.0: {} + denque@2.1.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -6948,12 +6664,7 @@ snapshots: dom-accessibility-api@0.6.3: {} - effect@3.21.4: - dependencies: - '@standard-schema/spec': 1.1.0 - fast-check: 3.23.2 - - effect@4.0.0-beta.99: + effect@4.0.0-beta.102(patch_hash=0e466320200ea28a3acd29dbedb9e0d8be0ce5659bfac9e4a17a96ad6235e63f): dependencies: '@standard-schema/spec': 1.1.0 fast-check: 4.9.0 @@ -7096,10 +6807,6 @@ snapshots: extend@3.0.2: {} - fast-check@3.23.2: - dependencies: - pure-rand: 6.1.0 - fast-check@4.9.0: dependencies: pure-rand: 8.4.2 @@ -7110,9 +6817,9 @@ snapshots: fast-levenshtein@2.0.6: {} - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 fetchdts@0.1.7: {} @@ -7161,7 +6868,7 @@ snapshots: h3@2.0.1-rc.20: dependencies: rou3: 0.8.1 - srvx: 0.11.16 + srvx: 0.11.22 happy-dom@20.10.6: dependencies: @@ -7171,7 +6878,7 @@ snapshots: buffer-image-size: 0.6.4 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.21.0 + ws: 8.21.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -7206,10 +6913,20 @@ snapshots: indent-string@4.0.0: {} - ini@4.1.3: {} - ini@7.0.0: {} + ioredis@5.11.1: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + is-core-module@2.16.1: dependencies: hasown: 2.0.3 @@ -7285,36 +7002,69 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -7331,6 +7081,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -7592,7 +7358,7 @@ snapshots: transitivePeerDependencies: - supports-color - mime@3.0.0: {} + mime@4.1.0: {} min-indent@1.0.1: {} @@ -7606,18 +7372,6 @@ snapshots: ms@2.1.3: {} - msgpackr-extract@3.0.3: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 - optional: true - msgpackr-extract@3.0.4: dependencies: node-gyp-build-optional-packages: 5.2.2 @@ -7630,19 +7384,13 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 optional: true - msgpackr@1.11.10: - optionalDependencies: - msgpackr-extract: 3.0.3 - msgpackr@2.0.4: optionalDependencies: msgpackr-extract: 3.0.4 - multipasta@0.2.7: {} - multipasta@0.2.8: {} - nanoid@3.3.13: {} + nanoid@3.3.16: {} natural-compare@1.4.0: {} @@ -7768,6 +7516,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + playwright-core@1.61.0: {} playwright@1.61.0: @@ -7776,9 +7526,9 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - postcss@8.5.15: + postcss@8.5.23: dependencies: - nanoid: 3.3.13 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -7796,8 +7546,6 @@ snapshots: punycode@2.3.1: {} - pure-rand@6.1.0: {} - pure-rand@8.4.2: {} react-aria-components@1.19.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): @@ -7828,7 +7576,7 @@ snapshots: react-devtools-core@7.0.1: dependencies: shell-quote: 1.10.0 - ws: 7.5.12 + ws: 7.5.13 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -7891,6 +7639,12 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -7998,8 +7752,12 @@ snapshots: srvx@0.11.16: {} + srvx@0.11.22: {} + stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + std-env@4.1.0: {} storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)): @@ -8082,8 +7840,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@2.0.0: {} @@ -8091,8 +7849,6 @@ snapshots: tinyspy@4.0.4: {} - toml@3.0.0: {} - toml@4.3.0: {} trim-lines@3.0.1: {} @@ -8123,7 +7879,7 @@ snapshots: undici-types@8.3.0: {} - undici@7.25.0: {} + undici@8.9.0: {} unified@11.0.5: dependencies: @@ -8185,8 +7941,6 @@ snapshots: dependencies: react: 19.2.7 - uuid@11.1.0: {} - uuid@14.0.1: {} vfile-message@4.0.3: @@ -8201,9 +7955,9 @@ snapshots: vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0): dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.23 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: @@ -8263,10 +8017,12 @@ snapshots: word-wrap@1.2.5: {} - ws@7.5.12: {} + ws@7.5.13: {} ws@8.21.0: {} + ws@8.21.1: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.1 @@ -8280,8 +8036,6 @@ snapshots: yallist@3.1.1: {} - yaml@2.8.3: {} - yaml@2.9.0: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5b52210f3d..dcbec06ffa 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -40,8 +40,8 @@ packages: - packages/@overeng/utils-dev patchedDependencies: - '@effect/platform@0.96.2': packages/@overeng/utils/patches/@effect__platform@0.96.0.patch '@myobie/pty@0.10.0': packages/@overeng/utils/patches/@myobie__pty@0.10.0.patch + effect@4.0.0-beta.102: packages/@overeng/utils/patches/effect@4.0.0-beta.102.patch allowUnusedPatches: true @@ -52,9 +52,6 @@ packageExtensions: peerDependencyRules: allowedVersions: - '@effect/experimental': '>=0.58.0' - '@effect/platform': '>=0.94.2' - '@effect/rpc': '>=0.73.0' eslint: '>=10.0.0' typescript: '>=6.0.0' vitest: '>=4.0.0' @@ -69,7 +66,7 @@ allowBuilds: sharp: false unix-dgram: false -minimumReleaseAgeExclude: ['@effect/platform', '@types/node', effect] +minimumReleaseAgeExclude: ['@effect/platform-node', '@types/node', effect] supportedArchitectures: os: [linux, darwin] diff --git a/pnpm-workspace.yaml.genie.ts b/pnpm-workspace.yaml.genie.ts index c5b4540fca..c52f814732 100644 --- a/pnpm-workspace.yaml.genie.ts +++ b/pnpm-workspace.yaml.genie.ts @@ -8,19 +8,6 @@ export default pnpmWorkspaceYaml.root({ repoName: 'effect-utils', catalogVersions: catalog, catalogDuplicateExceptions: [ - // LIVE-MIGRATION BRIDGE effect-3-4 B2 — DELETE at contraction — https://github.com/overengineeringstudio/effect-utils/issues/925 - { - package: 'effect', - versions: ['3.21.4', '4.0.0-beta.99'], - isolatedVersions: ['4.0.0-beta.99'], - // The inspector directly consumes Effect 4 schemas from LiveStore. Keep - // an exact Effect 4 development install for its tests while the published - // package uses the consumer's peer instance; never cast across majors. - reason: - '@overeng/react-inspector tests against Effect 4 while the repository catalog remains on Effect 3; published consumers provide Effect through its peer contract', - issue: '#937', - }, - // LIVE-MIGRATION END effect-3-4 { package: 'string-width', // @opentui/core@0.4.1 (latest) pins string-width@7.2.0 exactly, so pnpm @@ -34,6 +21,12 @@ export default pnpmWorkspaceYaml.root({ '@opentui/core@0.4.1 exact-pins string-width@7.2.0; not force-overridden because string-width 8 changes emoji/wide-char width logic that the TUI renderer relies on', issue: '#821', }, + { + package: '@opentelemetry/semantic-conventions', + versions: ['1.43.0', '1.41.1'], + reason: + '@effect/opentelemetry@4.0.0-beta.102 resolves semconv 1.43.0 in some injected workspace peer snapshots while the OTEL SDK cohort stays pinned to 1.41.1', + }, ], ...commonPnpmWorkspaceData, })