Skip to content

fix: address review findings on environment themes

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 28, 2026 in 1m 48s

Effect Service Conventions: 2 issues found

  • apps/server/src/cli/theme.ts:296-298 — ThemeFileUnreadableError is constructed with a manufactured cause: "no such file" on the path-shaped-but-missing branch, where no underlying failure exists. Make cause optional and omit it here so cause only ever carries a real error.
  • apps/server/src/cli/theme.ts:110-118 — ThemeIdInvalidError.fromFile is a boolean discriminator used solely to select the user-facing remedy in message; these should be two distinct tagged error classes, each deriving its message from its own attributes.

Previously flagged items are resolved: the CLI now defines a Schema.TaggedErrorClass per distinct failure with structured attributes and preserved causes, and environmentTheme.ts exports make alongside layer.

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 new/changed Effect service code in scope:

  • apps/server/src/environmentTheme.ts — canonical layout (imports, schema/predicate declarations, Context.Service tag with inline interface, exported make, export const layer = Layer.effect(...)), dependencies acquired from the environment (ServerConfig.ServerConfig, FileSystem.FileSystem), namespace imports from effect/* subpaths, named imports from @t3tools/contracts. No findings.
  • apps/server/src/server.ts, apps/server/src/ws.ts, apps/server/src/server.test.ts — consume the service via the module namespace (EnvironmentTheme.EnvironmentThemeService) and use EnvironmentThemeService["Service"] for the mock/test shape. No findings.
  • apps/server/src/cli/theme.ts — per-failure tagged errors with structured attributes and messages derived from those attributes; two findings noted above.
  • packages/contracts/src/server.ts, packages/contracts/src/settings.ts, packages/contracts/src/rpc.ts — schema/contract additions only; no service definitions.
  • apps/web/src/hooks/useEnvironmentTheme.ts, useDefaultTheme.ts, state/server.ts, themePalette.ts — React/atom layer; no ManagedRuntime.make, runPromise, runPromiseExit, service-instance parameters, or Layer.succeed hiding runtime-backed work were introduced anywhere in the changed scope.

Private const isEnvironmentThemeId = Schema.is(EnvironmentThemeId) constants in theme.ts and environmentTheme.ts match the prevailing repository pattern (unexported Schema.is constants at module scope, not wrapped in redundant functions) and were not flagged.


🔍 Agent activity — 6 tool calls

💭 Comparing the prior flagged commit against the new head, it looks like the separate TaggedErrorClass per failure and the exported make in environmentTheme.ts both address the earlier review comments. Now I want to check the current files directly for any remaining import or namespace issues. Scanning theme.ts and environmentTheme.ts, the imports and file structure (imports, error/schema declarations, tag, make, layer) look correctly ordered. The one thing that catches my eye is the private isEnvironmentThemeId predicate defined locally in both files rather than being exported once from a shared contracts module — that duplication seems worth flagging.

1. browse_code — apps/server/src/environmentTheme.ts

2. browse_code — apps/server/src/cli/theme.ts

💭 With both prior findings resolved, I'm now scanning for remaining violations, specifically looking at whether ThemeIdInvalidError's boolean fromFile field acts as a discriminator that should split into separate error classes per the convention. Filename-derived ids vs directly-provided ids really are different failures with different remedies, so I'll flag that as a low-severity split-class issue, though I'm not confident it should fail the check outright. Second, I'm looking at ThemeFileUnreadableError where the cause is a fabricated string "no such file" rather than a real underlying error — this directly violates the rule against manufacturing a cause, since there's no genuine failure being wrapped and the cause should instead be optional. I'm also noting that the missing-file case gets lumped into "unreadable" with a misleading "Could not read" message, which seems worth a concrete inline comment. The Schema.Defect() usage checks out since its fields structurally identify the error, but the private duplication of isEnvironmentThemeId = Schema.is(EnvironmentThemeId) across two modules instead of exporting it once from contracts looks like another flaggable convention gap. I'm checking naming conventions for the service tag key against other services, then looking at how the scoped effect and finalizer work in environmentTheme.ts given the Layer.effect scoping behavior in this version of Effect — that seems fine. Now I'm moving on to look at ws.ts. Everything checks out: the namespace import pattern is consistent, server.test.ts correctly uses Layer.mock with the Service key, and the web hook imports Equal properly. I'm noting that the module-global mutable state in themePalette.ts is a client-side store rather than an Effect service, so the "don't hide dependencies in module globals" rule doesn't really apply there. I'm now checking useDefaultTheme.ts for any Effect service usage — there isn't any — and scanning the diff for stray runPromise or ManagedRuntime calls, finding none. Next I want to look at how contracts/src/server.ts is styled, specifically whether there are existing patterns like Schema.is exports elsewhere in contracts to follow.

3. git_tools — Schema.is(

4. git_tools — = Schema.is(

💭 I'm noting that private Schema.is constants are a common convention, so I won't flag that duplication. Looking at the main issue, I'm concerned about fabricating a cause string like "no such file" for ThemeFileUnreadableError—this conflates a missing file with an unreadable one, though I need to check what the test actually asserts before deciding whether a separate error type is warranted. Since the test only checks for "Could not read" regardless of whether the file exists, the simplest fix is making cause optional rather than inventing a fake value, since this is exactly the legitimate case for an optional cause. I also want to flag the boolean discriminator in ThemeIdInvalidError.fromFile that picks the remedy message—per convention this should likely be split into separate error classes, one for direct argument errors and one for filename-derived ones, and I'll suggest that split while also checking the ws.ts changes and the environmentThemes gating logic to confirm those are behavioral rather than convention-related concerns. I'm now checking the server config for the environment themes directory addition and confirming the test layer setup is fine. I want to verify the naming and location conventions for the environment theme service relative to keybindings, check whether the scope/finalizer/fork pattern used for the file watcher counts as a hidden runtime concern, and compare the service tag string style and getter usage against how keybindings.ts handles similar patterns.

5. git_tools — Context.Service<

💭 I'm flagging that fabricated cause: "no such file" string in theme.ts line 297 — the convention against manufacturing errors to populate cause applies equally to fake string literals, so I want to note this as an inline comment. For the second finding, the fromFile boolean at lines 110-118 acts as a discriminator selecting the user-facing message, which fits the convention about splitting semantically distinct failures into separate error classes — I'll suggest that as a comment too. Neither issue feels severe enough to fail the check outright, since both are genuine but modest violations. Since inline comments need to target lines actually present in the diff, and this is a new file so every line qualifies, I'll place comment 1 on the cause construction around line 297 and comment 2 spanning lines 110-118 for the fromFile discriminator. For the cause fix, making it optional would touch both the field declaration and call site, so I'll just describe the fix in prose rather than provide a diff; keeping the error message "Could not read" intact while making cause optional is simpler than introducing a whole new error class.

6. modify_pr — Two error-modeling nits in apps/server/src/cli/theme.ts; the earlier findings (per-failure tagged errors, exported make in environmentTheme.ts) look addressed.


Agent Credits: 87 credits

Token usage: 10 uncached input · 281,199 cache-read input · 81,336 cache-write input · 6,841 output
Agent Credits may also include non-token charges from external tools such as web research.