Repository navigation
Effect Service Conventions: 1 issue found
apps/server/scripts/migrate-dev-db.ts(lines 170-176):Effect.mapError(() => new MigrateDevDbDestinationBusyError(...))wraps a realSqlErrorbut discards it, so the underlying error chain is lost and any non-lock failure is reported as "write-locked". Addcause: Schema.optional(Schema.Defect())to the error and pass the failure through ascause.
Details
Note
Your check run agent prompt is: .macroscope/check-run-agents/effect-service-conventions.md
More information about how Check Run Agents work can be found in our Help Center.
Reviewed the two added files in scope (apps/server/scripts/migrate-dev-db.ts, apps/server/scripts/migrate-dev-db.test.ts).
Checked and found compliant:
- Namespace imports from Effect subpaths (
effect/Effect,effect/FileSystem,effect/Path,effect/unstable/sql/SqlClient); no consolidated imports from"effect".import { Command, Flag } from "effect/unstable/cli"matches the established repository pattern across ~30 CLI entry points. - Local service module imported as a namespace (
NodeSqliteClient.layer);migrationManifest/runMigrationsare pure helpers, so named imports are appropriate. - No service definitions, so
Context.Service/make/layerordering rules do not apply; noFooShapetypes introduced. - No
ManagedRuntime.make,runPromise,runPromiseExit, orLayer.succeedfake DI.NodeRuntime.runMainappears only in theimport.meta.mainCLI boundary.SqlClientis acquired from the environment via provided layers;RunMigrateDevDbOptions.sharedHomeis pure configuration, not service injection. - Errors use
Schema.TaggedErrorClasswith structural attributes and messages derived from those attributes;MigrateDevDbPhaseErrorpairsSchema.Defect()with a multi-valuephasediscriminator plusdatabasePath, andMigrateDevDbSlotCollisionErrorcarries slot/name context. No raw payloads or credentials are copied into messages. The already-structuredMigrateDevDbSlotCollisionErroris passed through at the verify translation boundary rather than re-wrapped. wrapPhaseis retained as a reusable mapper adding phase/path context at four call sites, not a single-usenew SomeError(...)alias.- Tests cover new behavior (prune filter, slot collision, source/destination overlap, shared-home guard) rather than restating a mechanical refactor.
🔍 Agent activity — 1 tool call
💭 Looking at the diff for these new script files, I'm checking the import conventions they're using. They're importing from subpaths like
effect/unstable/cliwith named imports, but the codebase convention seems to prefer namespace imports from the main "effect" package or specific subpath modules. I need to see how the rest of the imports are structured to understand if there's a pattern violation here. The error classes look solid — they're usingSchema.TaggedErrorClassand structured discriminators properly. The interesting part is how they're handling error translation at boundaries. They're usingmapErrorwith a ternary to check the error tag and either pass through the structured domain error or create a new one, which aligns with the convention about preserving already-structured errors at translation boundaries, though it's done via ternary rather thancatchTags. For dependencies, they're importingNodeSqliteClient.layeras a namespace, which is correct for a service module, and pulling inrunMigrationsandmigrationManifestas named imports since those are pure helpers rather than services. They're also importingresolveWorktreeT3Homefrom the shared devHome module and using Node's built-inosmodule. At the CLI boundary, they're usingNodeRuntime.runMainfrom the platform-node runtime, which is the right place for it. No hidden runtimes or promise-based runners. The test file properly uses named imports from@effect/vitestand namespace imports where appropriate. Now I need to verify the actual conventions in the repo foreffect/unst The import style aligns with the repo's conventions. Looking at similar scripts like the t3-sqlite-state module and NodeSqliteClient to understand the established patterns, I notice the error handling usesEffect.mapErrorwith a ternary check on_tagrather thanEffect.catchTags, andverifyMigrationSlots()throws aMigrateDevDbSlotCollisionError. The approach here follows a sensible translation boundary pattern: structured domain errors that are part of the declared error channel pass through as-is, while only unknown or lower-level failures get wrapped. Thecause._tagnarrowing works fine and I wouldn't flag it. The other error types—MigrateDevDbPhaseErrorwithSchema.Defect()for the cause,MigrateDevDbSlotCollisionError, and the validation-only errors likeMigrateDevDbNotInWorktreeErrorandMigrateDevDbSharedHomeError—all look solid, and the pattern of returning them withyield*is consistent. For the dependency setup,Effect.provide(NodeSqliteClient.layer(...))inside the function is fine since this is a script managing multiple database files, not a service definition. But I'm noticing a potential issue:ensureNotInUseusesEffect.mapError()to createMigrateDevDbDestinationBusyErrorwithout preserving the underlying cause. The convention says when wrapping a real failure, thecauseshould be preserved alongside structural fields to keep the error chain intact—and if every construction wraps a failure,causeshould be required. HereMigrateDevDbDestinationBusyErrorhasdatabasePathandreasonbut nocause, and at least one construction site is wrapping a real error. On thereason: Schema.Stringfield, I'm checking whether it's a redundant discriminator. The convention warns against encoding the same distinction twice with both a specific error tag and a single-value literal. Herereasonis multi-value and gets interpolated into the message text, but it's using the same message template—just with different reason values plugged in. That's borderline acceptable since it's a diagnostic string rather than a semantic discriminator choosing between entirely different error classes. I'm noticing thatt3-sqlite-state.tspreserves the underlying error as acausewhen mapping errors, but the newensureNotInUsefunction drops it entirely. That's inconsistent with the established pattern of maintaining the original error context. The tricky part is thatMigrateDevDbDestinationBusyErrorgets constructed in two different ways — sometimes wrapping a SqlError, sometimes without any underlying failure — socauseneeds to be optional rather than required. This is a legitimate violation of the convention that says if every construction wraps a failure,causeshould be required. I should flag the missingcausepreservation as the main issue and keep the focus there rather than getting into whetherreasonshould use literal types. Looking at the other errors,MigrateDevDbPhaseError.phasecorrectly uses a multi-value discriminator withSchema.Literals, and thewrapPhasehelper is actually used 4 times so it's not a one-off alias — it's doing real work by currying the phase and path into the error constructor. ForverifyMigrationSlots, the current mapError with a ternary check on_tagis doing manual tag dispatch whenEffect.catchTagswould be the prescribed approach for handling statically known failures likeSqlErrorandMigrateDevDbSlotCollisionError. Checking the error channel more carefully — the sql template only throws SqlError, and the function explicitly returns MigrateDevDbSlotCollisionError, so the union is just those two. UsingEffect.catchTagsto handle SqlError and transform it to MigrateDevDbPhaseError would be cleaner and more aligned with the convention, making this a defensible flag. But wait, this is actually amapErrornot a catch — it's transforming one tag while passing another through, which does count as selective handling of known tags. I'll mention it as a suggestion but keep it low-key. Since the primary issue is dropping the cause entirely, which violates an explicit rule, I'll fail the check on that grounds. For the test file, the new behavior requires tests so that's fine. Now I need to verify the import style matches the repo convention. Checking through the test imports and other details: the vitest imports look good,withDatabaseprovides the layer correctly,Option.getOrUndefinedfor flags is fine, and the Node OS import with the diagnostic comment follows precedent. The import ordering also matches what I see elsewhere.Schema.Defect()in the phase error is allowed. One more thing to verify: whetherprocess.cwd()is being used directly in the migration function, which would violate the rule about hiding dependencies in module globals. I'm identifying two issues in the code. The first one is significant: around line 170-176, the error mapping is discarding the underlying SqlError instead of preserving it as acausefield, which violates the convention. I should check the repo's Schema API to see how to properly add an optional defect field and pass it through. The second finding is weaker and appears to be cut off, but it's related to some manual handling around line 411-415. I'm checking whethercatchTagshandlers can return yieldable errors directly or if I I'm drafting a code review with the primary issue being the missingcausefield, using the repo's standard pattern ofSchema.optional(Schema.Defect()). For the secondary point, I'll suggest usingEffect.catchTagsas a cleaner approach, and I'm verifying that passing throughMigrateDevDbSlotCollisionErroraligns with the translation boundary conventions that allow structured domain errors to flow through declared error channels. Actually, reconsidering whether to include that second finding — themapErrorwith ternary is a valid way to implement pass-through withoutcatchTags, and given the instruction to avoid optional style preferences, I'll keep the review focused on just the one critical finding about the missing cause. Scanning through the rest of the diff against service conventions, everything else checks out: no improper service definitions, the options parameter is just configuration, error handling is sound aside from the cause drop, and the test assertions are fine. Now I'm drafting the review comment itself. I'll point out that themapErroris swallowing the underlyingSqlError, which means non-lock failures get misreported as write-locked. The fix is to add an optionalcause` field to the error class and pass it through when constructing the new error, preserving the original failure reason. I'll post this with the line reference pointing to the mapError call.
1. modify_pr — One convention finding on the new script: a wrapped failure drops its underlying cause. Everything else (namespace imports, Schema.TaggedErrorClass errors with structural attributes and messages derived from them, SqlClient acquired from the environment, NodeRuntime.runMain only at the CLI boundary) matches the conventions.
Agent Credits: 102 credits