Skip to content

feat(server): vp run migrate-dev-db seeds worktree dev dbs with real …

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 9, 2026 in 5m 3s

Effect Service Conventions: 3 issues found

Three error-convention violations in apps/server/scripts/migrate-dev-db.ts (inline comments posted):

  • MigrateDevDbDestinationBusyError (lines 73-83): reason: Schema.String stores an unstructured message fragment as the error's only variable attribute, and the class carries no cause. Use a bounded literal discriminator plus cause: Schema.optional(Schema.Defect()).
  • ensureNotInUse (lines 159-165): Effect.mapError(() => ...) discards the underlying SqlError, losing the error chain and stack. Preserve it as cause.
  • runMigrateDevDb verify boundary (lines 382-386): hand-rolled _tag comparison inside mapError to pass a known tagged failure through; use Effect.catchTags({ SqlError: ... }) instead.

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.

Files examined in scope: apps/server/scripts/migrate-dev-db.ts, apps/server/scripts/migrate-dev-db.test.ts. Context consulted outside the diff: apps/server/src/persistence/Migrations.ts (migrationManifest), apps/server/src/persistence/NodeSqliteClient.ts (layer options), packages/shared/src/devHome.ts (resolveWorktreeT3Home returns string | undefined, so the undefined guard is correct), plus existing CLI files and error definitions across the repo for house style.

Checked and found compliant:

  • Effect modules imported as deep namespaces (effect/Effect, effect/FileSystem, effect/Path, effect/Schema, effect/unstable/sql/SqlClient); import { Command, Flag } from "effect/unstable/cli" matches the established repo-wide barrel style for the CLI module and was not flagged.
  • Local modules used through namespaces where they are service modules (NodeSqliteClient.layer), named imports only for pure helpers (runMigrations, migrationManifest, resolveWorktreeT3Home).
  • Dependencies acquired from the environment (yield* FileSystem.FileSystem, yield* Path.Path, yield* SqlClient.SqlClient); no service-instance parameters, no ManagedRuntime.make, no runPromise/runPromiseExit, no Layer.succeed hiding runtime-backed APIs. NodeRuntime.runMain is confined to the import.meta.main CLI boundary.
  • MigrateDevDbPhaseError uses a multi-value phase literal with a message derived only from structural attributes and keeps the real failure as cause — acceptable under the operation-discriminator rule, and no payloads/credentials are copied into attributes.
  • New tests cover real behavior (prune filter, slot collision, shared-home guard), so they are not "tests to prove a mechanical refactor".

🔍 Agent activity — 1 tool call

💭 I'm looking at the import conventions in the diff, specifically checking how the Effect library modules are being imported. The code uses a consolidated named import from "effect/unstable/cli" for Command and Flag, but the convention suggests importing Effect library modules from their subpaths as namespaces instead. For the error handling, MigrateDevDbPhaseError uses a multi-value operation discriminator with a generic message derived from the phase, which aligns with the allowed pattern. The use of Schema.Defect() for the cause is acceptable since the message incorporates both phase and databasePath to structurally identify the failure rather than just stringifying the cause. Now looking at MigrateDevDbDestinationBusyError — it stores reason as an unstructured string that drives the user-facing message, which violates the convention. The convention says to either split semantically distinct failures into separate error classes or use a Literals discriminator instead of a free-form string. Since reason is essentially an unstructured message field that's the only data, I should refactor this to use structured attributes with a discriminator rather than storing an arbitrary string. Also catching that ensureNotInUse discards the underlying SQL error when mapping to the custom error — the cause should be preserved alongside the structural fields instead of being dropped entirely. Looking at the rest of the code, the imports and dependency acquisition look fine — process.cwd() and NodeOS.homedir() are acceptable, and the module namespace usage is correct. The runMigrations import from the persistence module appears to be crossing a service boundary. For error handling, there's a pattern using _tag string comparison inside mapError to pass through already-structured domain errors, but the convention would prefer using Effect.catchTags to handle these statically known tagged failures instead. The test file looks fine since this is new behavior requiring test coverage. I'm seeing Schema.TaggedErrorClass being used correctly. The real question is whether this counts as an Effect service PR — it's creating a script that consumes the SqlClient service, so the error handling conventions definitely apply. I should check what the repo's import conventions are for this kind of work. The CLI import style follows the house standard, so that's fine. I need to check the error handling conventions in similar I'm examining how similar scripts handle error patterns, particularly looking at the ensureNotInUse function which currently drops the underlying SqlError when creating a MigrateDevDbDestinationBusyError. The convention should preserve the original error as a cause field, but the error class doesn't have that field defined—a clear gap in the implementation. For the reason field, it's currently just a free-form string, but the pattern should use structured attributes instead. The two call sites are hardcoding English strings like "write-locked" and "wal-held", which should be literal types or separate error classes rather than unstructured messages. I should flag both the missing cause field and the unstructured reason field in a single comment on the class definition. On the mapError call site, the code uses a manual _tag ternary to check for SqlError, but the convention prefers Effect.catchTags for catching statically known tagged failures. The idiomatic approach would be to use catchTags to wrap only the lower-level SqlError while passing through domain errors that are already part of the declared error channel. I'll note this as a minor inline comment since the current pattern mirrors the anti-pattern the guidelines warn against. The wrapPhase pattern itself is fine — it wraps the entire migrate phase with context about which phase failed and the database path, which provides useful information without being overly generic. And for the cause: Schema.Defect() in MigrateDevDbPhaseError, that's allowed since the error has both a tag and structured fields. Looking at dependency acquisition, process.cwd() and NodeOS.homedir() aren't violations — they're justified by the diagnostics context and don't hide dependencies in module globals. The test setup with withDatabase providing layers is fine, and the file layout here is a script rather than a service module, so error declaration order doesn't apply. The Effect.orElseSucceed that swallows errors in removeDatabaseFiles isn't a listed convention violation either. I'm looking at the diff line numbers for the review comments—since this is a new file, the right-side line numbers map directly to the actual file lines. For the first finding, I need to flag lines 73-83 where the class definition has reason as an unstructured string, which should instead be constrained to specific literal values like "write-locked" or "wal-held", and add an optional cause field for defect information. I'll post this suggestion on lines 76-82. I'm looking at how to handle tagged errors in the migration code—specifically whether to use a manual ternary in mapError or leverage Effect.catchTags with its handler pattern. The question is whether returning an error class instance directly works with catchTags since Schema.TaggedErrorClass extends Error and can be yielded. In Effect v4, errors need to be wrapped The SqlError tag is properly defined and used with catchTags elsewhere, which is good. I'm noticing that Effect.mapError in ensureNotInUse only handles SqlError on the error channel, so we're losing the cause information there—that's worth flagging. The other checks like the detail field requirement and the empty fields in MigrateDevDbNotInWorktreeError and MigrateDevDbSharedHomeError look fine. Looking at test conventions, the file uses it.layer and assert with error._tag checks, which aligns with the pattern. The MigrateDevDbDestinationBusyError with its reason string is interesting—since the reason gets interpolated into the user-facing message, the convention suggests either splitting this into separate error classes or making reason a literal discriminator with a generic message. I should propose both approaches with diffs and then draft the comments. Writing the first comment... I'm seeing that Schema.optional(Schema.Defect()) follows the established convention for handling causes. I need to post three inline review comments: one on line 77 about the missing cause parameter in the Schema.String definition, another flagging how mapError discards the cause on lines 159-165, and a third suggesting Effect.catchTags as a better approach for lines 382-386. For the first comment, I'm noting that storing reason as a prose fragment without preserving the underlying SqlError violates the error conventions — I should recommend using a bounded literal for the diagnostic message while keeping an optional cause field so the error chain survives. The message text can remain the same through mapping or by keeping the sentence in the getter per variant. Now I'm considering the simplest fix: changing reason to a literal union of the two actual failure modes ("write-locked" or "wal-held"), adding an optional cause field, and introducing a private getter that maps each literal to its full prose text. This keeps the message output unchanged while preserving the error chain and using a discriminator instead of an unstructured string. The concern is whether adding a lookup in the message getter violates the convention against large switch statements, but with only two variants it's borderline — I might just suggest the literal plus optional cause and leave the message handling flexible. Refactoring the error handling to use Effect.catchTags instead of manual tag comparison, which makes the code more declarative and ensures it stays exhaustive if new error types are added later.

1. modify_pr — Reviewed the new apps/server/scripts/migrate-dev-db.ts (and its test) against the Effect service/error conventions. Imports, Schema.TaggedErrorClass usage, the phase discriminator, and dependency acquisition (yield* SqlClient.SqlClient, layers provided per-database) all look right. Three error-modeling findings inline: the busy error stores a prose fragment as its only variable data and carries no cause, the wrap at the probe discards the underlying SqlError, and the verify boundary hand-rolls a _tag comparison instead of Effect.catchTags.


Agent Credits: 393 credits