Skip to content

fix(server): migrate-dev-db verifies migration slots before replacing…

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 9, 2026 in 2m 28s

Effect Service Conventions: 1 issue found

  • apps/server/scripts/migrate-dev-db.ts: MigrateDevDbDestinationBusyError.reason selects the user-facing message (server-running vs. the lock variants) and carries a variant-only optional pid; the server-running case should be its own error class with a required pid.

Previously flagged items are resolved in this revision: the busy error now carries a structured reason literal plus cause, and the slot verification uses Effect.catchTags({ SqlError: ... }).

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 reviewed:

  • apps/server/scripts/migrate-dev-db.ts
  • apps/server/scripts/migrate-dev-db.test.ts

Checked and found acceptable:

  • Namespaced effect/* subpath imports; { Command, Flag } from "effect/unstable/cli" matches the established repo-wide pattern for CLI scripts, and resolveWorktreeT3Home / runMigrations / migrationManifest are pure helpers, not service modules.
  • MigrateDevDbPhaseError uses a multi-value phase discriminator with a generic message derived from it, plus the database path and a real cause; wrapping happens per phase rather than around the whole pipeline.
  • Pure validation errors (SourceMissing, SourceIsDestination, SharedHome, NotInWorktree, SlotCollision) legitimately carry no cause and derive messages from structural fields.
  • No ManagedRuntime.make, runPromise, runPromiseExit, service-instance parameters, or hidden Layer.succeed runtime access; SQL access is provided via NodeSqliteClient.layer per phase.
  • Tests exercise new behavior (not a mechanical refactor) and use the real SQLite client with temp directories rather than mocking business logic.

🔍 Agent activity — 1 tool call

💭 The prior commit was force-pushed, so I'm looking at the current version to check for remaining issues. I've already verified that the reason literal, cause field, and catchTags were added correctly. Now I'm reviewing the imports to see if they follow the convention of importing Effect library modules from subpaths as namespaces rather than using named imports. I'm also examining the error classes—MigrateDevDbPhaseError uses a phase discriminator with five values to derive the message, which follows the multi-value discriminator pattern. But MigrateDevDbDestinationBusyError has a reason field with three values that branches to select the user-facing message, which might violate the convention about splitting semantically distinct failures into separate error classes when a discriminator drives the message or control flow. However, all three reasons represent the same semantic failure (destination busy), just with slightly different messaging—the server-running case includes a pid, while the others share a template. This is borderline, and since I previously suggested adding these literals, I'll skip flagging it now. The test file looks fine using it.layer(NodeServices.layer), and the new functionality justifies the additional tests. I'm also noting that isProcessAlive uses process.kill imperatively as a pure sync helper in a script, which is acceptable, though I should check whether module globals are hiding dependencies. I'm reviewing the script for convention violations, particularly around error modeling. Looking at MigrateDevDbPhaseError, it's a phase-based error with five possible values and a message derived from the phase, but it's being used as a generic wrapper for arbitrary causes. The issue is that the tag alone doesn't structurally identify the failure—it needs additional fields or a more specific tag to properly model the error according to convention. For MigrateDevDbDestinationBusyError, there's a potential violation: the message getter branches on the reason field (server-running vs write-locked/wal-held), and the convention suggests splitting semantically distinct failures into separate error classes when a discriminator is used to choose the user-facing message. The pid field only applies to server-running, and callers can see the reason field, so this might warrant separate error classes rather than one error with conditional messaging. The wrapPhase helper is used five times and adds meaningful phase and path context, so it's reusable enough to justify as a helper rather than a one-off curried alias. Looking at the rest: ensureNotInUse now includes the cause in the busy error, which is good. pruneSnapshot uses unsafe SQL with table names from a fixed list, which is fine. No ManagedRuntime or runPromise calls. Imports look reasonable. The schema for the cause field matches what I suggested. The error classes are exported from a script as expected. One test using it.effect is failing. Looking at the diff to understand what's being changed with the error classes. I'm checking whether MigrateDevDbPhaseError properly wraps the underlying Schema.Defect() cause with its phase discriminator—it has five possible values and derives the message from the phase and path, which aligns with the allowed model. The wrapPhase helper is a curried function that constructs a new PhaseError through mapError, and while it technically just wraps construction, it's used across six different phases so it's not a one-off alias. The MigrateDevDbDestinationBusyError is trickier though—its message getter branches on the reason field to produce different user-facing messages, and the new "server-running" case introduces an optional pid field that only applies to that one reason with a completely distinct message, which looks like it violates the rule about splitting semantically distinct failures into separate error classes. The commit 961116f introduced a new "server-running" reason with a message-selecting discriminator and a variant-specific pid field, which is a legitimate new finding that fits the pattern of splitting semantically distinct failures. I'm also noticing there's an existing serverRuntimeState.ts module in the server codebase that defines PersistedServerRuntimeState and has schema parsing utilities, but the script is redeclaring its own ServerRuntimeState instead of reusing that. I'm noticing there's already a canonical readPersistedServerRuntimeState function and PersistedServerRuntimeState schema defined in the server runtime state module, but the script is duplicating this by declaring its own ServerRuntimeState schema and read logic. This violates the principle of keeping a service's schemas and contracts in one place — the script should import and use the canonical reader instead, which already handles logging warnings and returning Option.none on decode failures. The complication is that the canonical schema requires additional fields like port, origin, and startedAt that the test fixture doesn't include, so switching to the canonical decoder wouldn't be a straightforward drop-in replacement. Still, duplicating a persisted file format schema risks drift over time. While the script's intentional loose slice is defensible as a standalone CLI tool under apps/server/scripts, reusing the canonical reader would be the cleaner approach. Now I'm weighing the DestinationBusyError reason branching against the convention guidelines. The tension is between two competing principles: one says to avoid encoding the same distinction twice with both a specific error tag and a discriminator field, while the other says to split semantically distinct failures into separate error classes when a discriminator drives the user-facing message or caller control flow. The message getter here selects between different messages based on the reason field. Checking the rest of the error classes and imports — MigrateDevDbSourceMissingError and similar are pure validation errors without causes, which is fine. The test file properly uses the layer pattern and narrows on _tag, and MigrateDevDbNotInWorktreeError has no fields with a static message. The imports follow the repo convention of namespacing service modules while keeping pure helpers like migrationManifest and runMigrations unnamespaced, and Schema.Defect() is already used elsewhere in the codebase. I'm thinking about adding a comment to the code around lines 88-95 suggesting we create a separate MigrateDevDbServerRunningError exception that requires a pid parameter, while keeping the existing busy error for write-locked and wal-held states. I should sketch out a unified diff showing just

1. modify_pr — One finding: the destination-busy error uses its reason discriminator to select the user-facing message and carries a variant-only pid. Earlier feedback on cause, the structured reason, and Effect.catchTags is addressed.


Agent Credits: 124 credits