Skip to content

fix(web): preserve demo catalog during seed migration

MacroscopeApp / Macroscope - Effect Service Conventions failed Jul 30, 2026 in 2m 30s

Effect Service Conventions: 1 issue found

1 finding

  • apps/web/src/demo/server.ts (lines 187, 452-458): the demo dispatch invariant is a plain Error subclass carrying only an unstructured message, and dispatchDemoCommand copies cause.message (or any arbitrary defect's text) into OrchestrationDispatchCommandError.message. Service failures should be Schema.TaggedErrorClass with structured attributes, with the wrapper message derived from those attributes and the original failure preserved only as cause.

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.tsx
  • apps/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.ts
  • apps/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/socket and effect/unstable/rpc use the same named-namespace form as apps/server/src/ws.ts. Contracts and shared packages use named imports, which is allowed.
  • Service definition: no Context.Service tag is added, moved, or refactored; no standalone FooShape interfaces or compatibility re-export shims were introduced, and no service module was split across Services//Layers/ paths.
  • Dependency acquisition / runtime boundaries: no ManagedRuntime.make, runPromise, or runPromiseExit in the new code. Effect.runFork(Layer.launch(...)) in startDemoServer() runs at the demo entry point (src/demo/main.tsx), an explicit application boundary. Layer.succeed(SocketServer.SocketServer, ...) provides a record whose run builds its Effects lazily rather than performing imperative work during layer construction, so no asynchronous dependency is hidden.
  • Errors and predicates: the reused unsupported/unsupportedError helpers 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.acceptor from a module-level constant in demoBackends, which is fixture data rather than an Effect service, so that's acceptable here. In startDemoServer(), I'm using Effect.runFork to launch the server layer — this is an application boundary in the main entry point, and runFork isn't in the flagged list of problematic runtime calls, so that's fine. For errors, unsupportedError creates an EnvironmentAuthorizationError with a message derived from the method name — that's using an existing contract error, not defining a new class. The dispatchDemoCommand function maps errors to OrchestrationDispatchCommandError... Actually, I'm noticing something here. The convention says to derive error messages exclusively from stable structural attributes, never from cause.message. But in this case, the error class has a required message field 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, DemoCommandInvariantError is just a plain Error subclass rather than using Schema.TaggedErrorClass with 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 by Effect.try, I'm wondering if the convention about using TaggedErrorClass even 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 that DemoCommandInvariantError extends Error directly instead of using Schema.TaggedErrorClass like the production server's OrchestrationCommandInvariantError does. The second issue is that OrchestrationDispatchCommandError derives 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 required message field 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 that DemoCommandInvariantError is 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 like commandType and detail. So flagging this as a convention violation seems reasonable—it's exactly the pattern the conventions warn against: using plain error classes instead of TaggedErrorClass with 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 where DemoCommandInvariantError extends Error and 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. because Effect.try's catch handler receives unknown, the code's cause instanceof Error check 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 into message. That's the strongest argument for Finding A. I'll report it as an inline comment on the dispatchDemoCommand lines covering both the plain-Error class and the message derivation, with a secondary note on the plain Error instantiation. The demoVcsStore is 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. installDemoNetworkInterceptors patches globals outside Effect conventions, and Effect.runFork in startDemoServer is acceptable since it's at the application boundary in main.tsx. For the remaining candidates, seed.ts uses raw Promises and indexedDB imperatively without wrapping in Effect services, which is fine for fixture setup. fixtures.ts uses Schema.decodeUnknownSync and DateTime.makeUnsafe without 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 the unsupported helper that constructs an error with a method name and adds the requiredScope field 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 restructuring DemoCommandInvariantError as a Schema.TaggedErrorClass with 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