Repository navigation
Effect Service Conventions: 1 issue found
1 finding
apps/web/src/demo/server.ts(lines 187, 452-458): the demo dispatch invariant is a plainErrorsubclass carrying only an unstructured message, anddispatchDemoCommandcopiescause.message(or any arbitrary defect's text) intoOrchestrationDispatchCommandError.message. Service failures should beSchema.TaggedErrorClasswith structured attributes, with the wrapper message derived from those attributes and the original failure preserved only ascause.
No other convention violations were found in the changed TypeScript.
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.
Scope examined
In-scope changed TypeScript/TSX:
apps/web/src/demo/server.ts,server.test.ts,fixtures.ts,fixtures.test.ts,seed.ts,stage.ts,main.tsx,stageArt.tsxapps/web/src/branding.logic.ts,branding.test.ts,routes/__root.tsx,components/sidebar/SidebarChrome.tsx,components/settings/ConnectionsSettings.tsx,vite-env.d.ts,vite.demo.config.tsapps/marketing/src/lib/releases.ts,releases.test.ts,site.ts,vercel.ts
Checks performed
- Imports: Effect modules are imported as namespaces from their subpaths (
effect/Effect,effect/Layer,effect/Queue,effect/Stream,effect/Schema,effect/DateTime);effect/unstable/socketandeffect/unstable/rpcuse the same named-namespace form asapps/server/src/ws.ts. Contracts and shared packages use named imports, which is allowed. - Service definition: no
Context.Servicetag is added, moved, or refactored; no standaloneFooShapeinterfaces or compatibility re-export shims were introduced, and no service module was split acrossServices//Layers/paths. - Dependency acquisition / runtime boundaries: no
ManagedRuntime.make,runPromise, orrunPromiseExitin the new code.Effect.runFork(Layer.launch(...))instartDemoServer()runs at the demo entry point (src/demo/main.tsx), an explicit application boundary.Layer.succeed(SocketServer.SocketServer, ...)provides a record whoserunbuilds its Effects lazily rather than performing imperative work during layer construction, so no asynchronous dependency is hidden. - Errors and predicates: the reused
unsupported/unsupportedErrorhelpers add fixed scope/context across ~40 handlers, which is legitimate reusable normalization rather than a bare error-constructor alias. The one issue is the dispatch-boundary wrapping reported above. - Change discipline: comments and invariants are preserved; tests added cover demo fixture/store/git-action behavior rather than proving a mechanical refactor.
One inline review comment was posted on apps/web/src/demo/server.ts.
🔍 Agent activity — 1 tool call
💭 I'm reviewing the TypeScript files that have changed across the apps directory to check if they follow Effect service conventions, looking at files like the releases utilities, site configuration, branding logic, and various component files. The main Effect-related code is in the demo server setup, which imports Effect modules as namespaces from their subpaths—Effect, Layer, Queue, Stream—along with Socket and SocketServer from the unstable socket module. These are following the convention of importing Effect library modules from their subpaths as namespaces rather than as default imports. Looking at the other files, fixtures.ts and seed.ts both use namespace imports from effect/DateTime and effect/Schema, which is correct. The test file imports from @effect/vitest and uses the standard Effect namespace imports as well. The service definitions in server.ts use plain classes like DemoShellStore and DemoSettingsStore rather than Context.Service, and Layer.succeed is used to provide implementations for existing services like SocketServer. SocketServer. The layer construction captures
backend.acceptorfrom a module-level constant in demoBackends, which is fixture data rather than an Effect service, so that's acceptable here. InstartDemoServer(), I'm usingEffect.runForkto launch the server layer — this is an application boundary in the main entry point, andrunForkisn't in the flagged list of problematic runtime calls, so that's fine. For errors,unsupportedErrorcreates anEnvironmentAuthorizationErrorwith a message derived from the method name — that's using an existing contract error, not defining a new class. ThedispatchDemoCommandfunction maps errors toOrchestrationDispatchCommandError... Actually, I'm noticing something here. The convention says to derive error messages exclusively from stable structural attributes, never fromcause.message. But in this case, the error class has a requiredmessagefield by contract, and I'm deriving it from the cause's message. The test expects the message to match what the real server produces, so it's mirroring actual behavior — but I need to think through whether this violates the convention or if it's acceptable given the pre-existing error class contract. Also,DemoCommandInvariantErroris just a plainErrorsubclass rather than usingSchema.TaggedErrorClasswith structured attributes. Since this is a new error class in a demo module that's thrown synchronously inside a plain class (not an Effect service) and caught byEffect.try, I'm wondering if the convention about usingTaggedErrorClasseven applies here. Let me check how the real server handles this pattern to see if my approach is consistent with the codebase. I need to check the existing dispatch command wrapping pattern in ws.ts around line 575 to compare it with what the demo is doing. I need to check the actual mapping in ws.ts around those line numbers to see how the demo is structured. I need to check the specific error construction patterns in those line ranges of ws.ts to understand how dispatch errors are actually being built. I'm identifying potential issues in the demo module. The first concern is thatDemoCommandInvariantErrorextendsErrordirectly instead of usingSchema.TaggedErrorClasslike the production server'sOrchestrationCommandInvariantErrordoes. The second issue is thatOrchestrationDispatchCommandErrorderives its message from the cause's message, which violates the pattern of creating a wrapper message independently. I'm noticing that the demo constructs the error message by pulling from the cause if it's an Error instance, which violates the convention that the wrapper's message should only come from its own stable attributes, not from the cause. The question is whether this demo module actually qualifies as an "Effect service" where these rules would apply. Looking at what the module does—it implements the WsRpcGroup handlers, constructs Layers, and consumes the SocketServer service—it's clearly operating within the Effect ecosystem, so the conventions should apply. But there's a nuance here: the contract error has a requiredmessagefield rather than a derived getter, meaning the caller has to provide the message string. Given that constraint, pulling from the cause's message might seem like the only practical option. Still, that's the core violation—the message is being sourced from internal error details rather than from the error class's own structural attributes. The real issue is thatDemoCommandInvariantErroris a new error class storing just an unstructured message with no other data, then passing that message up through the wrapper. The production equivalent,OrchestrationCommandInvariantError, has structured fields likecommandTypeanddetail. So flagging this as a convention violation seems reasonable—it's exactly the pattern the conventions warn against: using plain error classes instead ofTaggedErrorClasswith structured attributes. Though technically it's an internal control-flow signal in a mock that gets caught immediately, the principle still applies. I should look at the actual code to pin down the exact lines and see what's really happening. I'm evaluating an approach whereDemoCommandInvariantErrorextendsErrorand copies the message from the underlying cause, but this violates the core principle that error messages should be derived from structured attributes rather than pulled directly from the cause—the wrapper needs to construct its own message based on its stable schema properties. becauseEffect.try's catch handler receivesunknown, the code'scause instanceof Errorcheck will silently catch arbitrary defects like a TypeError inside the store and surface their raw message to clients as if it were a domain error — which directly violates the rule against copying arbitrary defect text intomessage. That's the strongest argument for Finding A. I'll report it as an inline comment on thedispatchDemoCommandlines covering both the plain-Error class and the message derivation, with a secondary note on the plain Error instantiation. ThedemoVcsStoreis a module-level singleton for fixture state rather than an Effect service, so it doesn't violate the dependency-hiding rule—flagging it would be overly strict.installDemoNetworkInterceptorspatches globals outside Effect conventions, andEffect.runForkinstartDemoServeris acceptable since it's at the application boundary in main.tsx. For the remaining candidates,seed.tsuses raw Promises and indexedDB imperatively without wrapping in Effect services, which is fine for fixture setup.fixtures.tsusesSchema.decodeUnknownSyncandDateTime.makeUnsafewithout issue. Now I'm checking how the codebase imports from the unstable socket and RPC modules to see if there's a consistent pattern I should follow. Checking whether theunsupportedhelper that constructs an error with a method name and adds therequiredScopefield violates the guideline against trivial error constructors — it's used frequently enough (~40 times) that it seems to provide meaningful value. Given this one moderate finding about error construction patterns in the new Effect code, I should fail the check and report it as an inline comment. I'm drafting a comment that suggests restructuringDemoCommandInvariantErroras aSchema.TaggedErrorClasswith explicit fields rather than relying on unstructured messages, so the wrapper can build its message from those structured fields while preserving the original error as the cause. Since the fix spans multiple locations (class definition, throw site, and mapper), I won't include a diff per the guidelines. I'll post the comment starting around line 452 to cover the Effect.try block, with a range through line 458, and reference line 187 where the error is thrown.
1. modify_pr — Reviewed the new Effect code in apps/web/src/demo/* (in-browser demo RPC backend, layers, fixtures, seeding) plus the touched web/marketing TypeScript against the Effect service conventions. Namespace subpath imports, layer composition, WsRpcGroup.toLayer handler wiring, and the Effect.runFork(Layer.launch(...)) application boundary all look consistent with the conventions. One finding on error modeling at the dispatch boundary in apps/web/src/demo/server.ts.
Agent Credits: 304 credits