opencode-effect-enforcer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,43 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: avoid-process-env
6
+ description: Avoid process.env - use Effect Config.* for environment variable access
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern: process.env
10
+ level: warning
11
+ suggestSkills:
12
+ - effect-config
13
+ ---
14
+
15
+ # Avoid `process.env`
16
+
17
+ ```haskell
18
+ -- Transformation
19
+ processEnv :: String -> IO (Maybe String) -- side effect, untyped, untestable
20
+
21
+ -- Instead
22
+ Config.string :: String -> Config String -- typed, composable, testable
23
+ Config.withDefault :: a -> Config a -> Config a
24
+ Config.redacted :: String -> Config Redacted -- for sensitive values
25
+ ```
26
+
27
+ ```haskell
28
+ -- Pattern
29
+ bad :: Effect String
30
+ bad = Effect.sync \_ -> process.env.API_KEY -- raw side effect
31
+
32
+ good :: Effect String ConfigError
33
+ good = Config.string "API_KEY" -- typed, validated
34
+
35
+ better :: Effect String ConfigError
36
+ better = Config.string("PORT")
37
+ & Config.withDefault "3000"
38
+ & Config.map Number.parse -- with transformation
39
+ ```
40
+
41
+ `process.env` is a raw side effect with no type safety. Use `Config.*` for validated, composable, testable configuration.
42
+
43
+ Exception: `xdg/core` and similar platform abstraction layers that ARE the env variable bridge.
@@ -0,0 +1,73 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: avoid-react-hooks
6
+ description: React hooks (useState, useEffect, useReducer, etc.) should be avoided - use View Models with Effect Atom instead
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern:
10
+ - 'useState($$$)'
11
+ - 'useState<$$$>($$$)'
12
+ - 'useEffect($$$)'
13
+ - 'useEffect<$$$>($$$)'
14
+ - 'useReducer($$$)'
15
+ - 'useReducer<$$$>($$$)'
16
+ - 'useCallback($$$)'
17
+ - 'useCallback<$$$>($$$)'
18
+ - 'useMemo($$$)'
19
+ - 'useMemo<$$$>($$$)'
20
+ - 'useRef($$$)'
21
+ - 'useRef<$$$>($$$)'
22
+ - 'useLayoutEffect($$$)'
23
+ - 'useImperativeHandle($$$)'
24
+ - 'useDebugValue($$$)'
25
+ - 'useDeferredValue($$$)'
26
+ - 'useTransition($$$)'
27
+ - 'useId($$$)'
28
+ - 'useSyncExternalStore($$$)'
29
+ - 'useInsertionEffect($$$)'
30
+ level: high
31
+ suggestSkills:
32
+ - effect-react-vm
33
+ ---
34
+
35
+ # Avoid React Hooks - Use View Models
36
+
37
+ ```haskell
38
+ -- Transformation
39
+ useState :: a → (a, a → ()) -- scattered state, untestable
40
+ useEffect :: (() → ()) → [a] → () -- cleanup error-prone
41
+
42
+ -- Instead: View Model pattern
43
+ data VM = VM
44
+ { state$ :: Atom State -- reactive state
45
+ , action :: () → Effect () -- effectful actions
46
+ }
47
+
48
+ -- Component is pure renderer
49
+ component :: VM → JSX
50
+ component vm = useAtomValue (state$ vm) -- only reads atoms
51
+ ```
52
+
53
+ ```haskell
54
+ -- Replacements
55
+ useState → vmAtom :: Atom a
56
+ useEffect → vmAction :: Effect ()
57
+ useCallback → derivedAtom :: Atom (a → b)
58
+ useMemo → derivedAtom :: Atom a
59
+ useRef (DOM) → pass from parent ∨ VM trigger
60
+ useSearchParams → Atom.searchParam
61
+ useEffect (cleanup) → Atom.make with get.addFinalizer
62
+
63
+ -- Architecture
64
+ data Component = Component
65
+ { view :: VM → JSX -- pure renderer
66
+ , vm :: Layer VM -- testable, injectable
67
+ }
68
+
69
+ -- Invoke skill for implementation
70
+ invoke "react-vm"
71
+ ```
72
+
73
+ React hooks scatter state across components. Use View Models: state in atoms, effects in actions, components as pure renderers.
@@ -0,0 +1,45 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: avoid-schema-suffix
6
+ description: Schema constants should be named after the domain type, not suffixed with Schema
7
+ glob: '**/*.{ts,tsx}'
8
+ pattern: \b(const|let)\s+\w+Schema\s*=\s*Schema\.
9
+ level: info
10
+ ---
11
+
12
+ # Name Schemas After the Domain Type
13
+
14
+ ```haskell
15
+ -- Transformation
16
+ const UserSchema = Schema.String -- redundant suffix, not the domain name
17
+ const User = Schema.String -- named after the domain concept
18
+
19
+ -- Pattern
20
+ bad :: Schema naming
21
+ bad = const UserSchema = Schema.Struct({ ... })
22
+ bad = const OrderIdSchema = Schema.String
23
+ bad = export const PayloadSchema = Schema.Class(...)
24
+
25
+ good :: Schema naming
26
+ good = const User = Schema.String
27
+ good = export const OrderId = Schema.String
28
+ good = class Payload extends Schema.Class<Payload>("Payload")({ ... })
29
+ ```
30
+
31
+ ```haskell
32
+ -- For non-class schemas, export type alias with same name
33
+ export const OrderId = Schema.String
34
+ export type OrderId = typeof OrderId.Type
35
+
36
+ -- For class schemas, the name is built-in
37
+ export class User extends Schema.Class<User>("User")({
38
+ id: Schema.String,
39
+ name: Schema.String
40
+ }) {}
41
+ ```
42
+
43
+ Schema constants should be named after the domain type they represent, not suffixed with `Schema`. The `Schema` suffix is redundant since the value itself is already a schema — the name should communicate what it models, not what it is.
44
+
45
+ References: EF-3, Checklist #8 in effect-first-development.md
@@ -0,0 +1,68 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: avoid-sync-fs
6
+ description: Avoid synchronous filesystem operations
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern:
10
+ - 'readFileSync($$$)'
11
+ - '$A.readFileSync($$$)'
12
+ - 'writeFileSync($$$)'
13
+ - '$A.writeFileSync($$$)'
14
+ - 'mkdirSync($$$)'
15
+ - '$A.mkdirSync($$$)'
16
+ - 'readdirSync($$$)'
17
+ - '$A.readdirSync($$$)'
18
+ - 'statSync($$$)'
19
+ - '$A.statSync($$$)'
20
+ - 'existsSync($$$)'
21
+ - '$A.existsSync($$$)'
22
+ - 'copyFileSync($$$)'
23
+ - '$A.copyFileSync($$$)'
24
+ - 'unlinkSync($$$)'
25
+ - '$A.unlinkSync($$$)'
26
+ - 'rmdirSync($$$)'
27
+ - '$A.rmdirSync($$$)'
28
+ - 'renameSync($$$)'
29
+ - '$A.renameSync($$$)'
30
+ - 'appendFileSync($$$)'
31
+ - '$A.appendFileSync($$$)'
32
+ level: high
33
+ suggestSkills:
34
+ - effect-filesystem
35
+ ---
36
+
37
+ # Avoid Synchronous Filesystem Operations
38
+
39
+ ```haskell
40
+ -- Transformation
41
+ readFileSync :: FilePath → IO String -- blocks event loop
42
+ writeFileSync :: FilePath → String → IO () -- same problem
43
+
44
+ -- Instead
45
+ readFileString :: FilePath → Effect String FileSystem
46
+ writeFileString :: FilePath → String → Effect () FileSystem
47
+ ```
48
+
49
+ ```haskell
50
+ -- Pattern
51
+ bad :: FilePath → IO String
52
+ bad path = fs.readFileSync path "utf-8" -- blocking, defeats async
53
+
54
+ good :: FilePath → Effect String FileSystem
55
+ good path = do
56
+ fs ← FileSystem.FileSystem
57
+ fs.readFileString path -- non-blocking, composable
58
+
59
+ -- Sync → Async mapping
60
+ readFileSync → readFileString
61
+ writeFileSync → writeFileString
62
+ mkdirSync → makeDirectory
63
+ existsSync → exists
64
+ unlinkSync → remove
65
+ readdirSync → readDirectory
66
+ ```
67
+
68
+ Sync operations block the event loop. Use Effect's FileSystem service for async, composable file operations.
@@ -0,0 +1,47 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: avoid-try-catch
6
+ description: Avoid try-catch blocks in Effect code - use Effect.try or typed errors
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern: 'try { $$$ }'
10
+ level: warning
11
+ suggestSkills:
12
+ - effect-error-handling
13
+ ---
14
+
15
+ # Avoid `try { } catch` in Effect Code
16
+
17
+ ```haskell
18
+ -- Transformation
19
+ tryCatch :: IO a → IO (Either Error a) -- loses error types
20
+ effectTry :: (() → a) → Effect a E -- preserves error channel
21
+
22
+ -- Error handling
23
+ catchTag :: Tag → (E → Effect a) → Effect a E → Effect a (E - Tag)
24
+ catchTags :: {Tag₁: h₁, Tag₂: h₂} → Effect a E → Effect a (E - Tags)
25
+ ```
26
+
27
+ ```haskell
28
+ -- Pattern
29
+ bad :: () → { success :: Bool, data :: Maybe a, error :: Maybe String }
30
+ bad () = try
31
+ result ← riskyOperation
32
+ pure { success: True, data: Just result, error: Nothing }
33
+ catch e →
34
+ pure { success: False, data: Nothing, error: Just (show e) }
35
+
36
+ good :: Effect a DataError
37
+ good = Effect.try
38
+ { try: riskyOperation
39
+ , catch: DataError <<< show
40
+ }
41
+
42
+ -- Or with TaggedError
43
+ data DataError = DataError { message :: String }
44
+ deriving Schema.TaggedError "DataError"
45
+ ```
46
+
47
+ `try-catch` breaks the error channel—errors become opaque. Use `Effect.try` with `Schema.TaggedError` for typed, composable error handling.
@@ -0,0 +1,38 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: avoid-ts-ignore
6
+ description: Avoid using @ts-ignore or @ts-expect-error to silence type errors
7
+ glob: '**/*.{ts,tsx}'
8
+ pattern: @ts-(ignore|expect-error)
9
+ level: warning
10
+ matchInComments: true
11
+ ---
12
+
13
+ # Avoid `@ts-ignore` and `@ts-expect-error`
14
+
15
+ ```haskell
16
+ -- Anti-pattern
17
+ tsIgnore :: TypeError → () -- hide error, pray at runtime
18
+ tsExpect :: TypeError → () -- same with false confidence
19
+
20
+ -- Instead
21
+ fix :: TypeError → Code → Code -- address root cause
22
+ guard :: Unknown → Maybe Known -- runtime validation
23
+ schema :: Schema a → Unknown → Either ParseError a
24
+ ```
25
+
26
+ ```haskell
27
+ -- Pattern
28
+ bad :: Effect a
29
+ bad = do
30
+ -- @ts-ignore
31
+ x ← brokenCode -- compiler is wrong, right?
32
+
33
+ good :: Effect a
34
+ good = do
35
+ x ← Schema.decode schema raw -- prove correctness
36
+ ```
37
+
38
+ Suppressing errors masks bugs that surface at runtime. Fix the underlying type issue instead.
@@ -0,0 +1,67 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: avoid-untagged-errors
6
+ description: Review raw Error usage; recoverable domain failures should use Schema.TaggedError
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - all:
12
+ - pattern: 'new Error($$$)'
13
+ - not:
14
+ inside:
15
+ pattern: Effect.die($$$)
16
+ stopBy: end
17
+ - pattern: '$A instanceof Error'
18
+ level: info
19
+ suggestSkills:
20
+ - effect-error-handling
21
+ ---
22
+
23
+ # Avoid `instanceof Error` and `new Error`
24
+
25
+ ```haskell
26
+ -- Transformation
27
+ instanceofError :: Error → Bool -- opaque, no discrimination
28
+ newError :: String → Error -- untagged, untrackable
29
+
30
+ -- Instead
31
+ data MyError = MyError { message :: Schema.String }
32
+ deriving Schema.TaggedError "MyError"
33
+
34
+ taggedFail :: MyError → Effect a MyError
35
+ catchTag :: "MyError" → (MyError → Effect a) → Effect a E → Effect a (E - MyError)
36
+ ```
37
+
38
+ ```haskell
39
+ -- Pattern
40
+ bad :: Error → Effect ()
41
+ bad e
42
+ | e `instanceof` Error = log (message e) -- which error type?
43
+ | otherwise = pure ()
44
+
45
+ good :: Effect () MyError
46
+ good = pipe
47
+ myEffect
48
+ $ catchTag "MyError" \e → log (message e)
49
+
50
+ -- Exhaustive handling
51
+ handle :: Effect a (E₁ | E₂ | E₃) → Effect a ∅
52
+ handle = catchTags
53
+ { E₁: handler₁
54
+ , E₂: handler₂
55
+ , E₃: handler₃
56
+ }
57
+ ```
58
+
59
+ `Schema.TaggedError` enables exhaustive pattern matching via `_tag`, serialization, and RPC compatibility. Use `catchTag` for type-safe error discrimination.
60
+
61
+ Exceptions:
62
+
63
+ - `new Error(...)` inside `Effect.die(...)` for impossible states or programmer bugs
64
+ - invariant branches inside runtime adapters where the failure should remain a defect
65
+ - interop callbacks that must produce a raw throwable before being re-captured at the boundary
66
+
67
+ Do not use those exceptions for user-facing or recoverable domain failures. Because syntax alone cannot establish whether a raw error is a defect, interop value, or recoverable failure, this diagnostic is informational and requires contextual review.
@@ -0,0 +1,46 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: avoid-yield-ref
6
+ description: Do not yield* Ref/Deferred/Fiber/Latch directly — use explicit method calls
7
+ glob: '**/*.ts'
8
+ detector: ast
9
+ rule:
10
+ pattern: yield* $RESOURCE
11
+ constraints:
12
+ RESOURCE:
13
+ regex: '(^ref$|Ref$|^deferred$|Deferred$|^fiber$|Fiber$|^latch$|Latch$)'
14
+ level: warning
15
+ suggestSkills:
16
+ - effect-schema-v4
17
+ ---
18
+
19
+ # Do Not `yield*` Ref, Deferred, Fiber, or Latch Directly
20
+
21
+ ```haskell
22
+ -- Transformation
23
+ yield* ref :: Ref a -> a -- removed in v4
24
+ Ref.get(ref) :: Ref a -> Effect a -- correct, explicit
25
+ yield* deferred :: Deferred a -> a -- removed in v4
26
+ Deferred.await :: Deferred a -> Effect a -- correct, explicit
27
+ yield* fiber :: Fiber a -> a -- removed in v4
28
+ Fiber.join :: Fiber a -> Effect a -- correct, explicit
29
+ ```
30
+
31
+ ```haskell
32
+ -- Pattern
33
+ bad :: Direct yield
34
+ bad = const value = yield* ref
35
+ bad = yield* deferred
36
+ bad = yield* fiber
37
+ bad = yield* latch
38
+
39
+ good :: Explicit method
40
+ good = const value = yield* Ref.get(ref)
41
+ good = yield* Deferred.await(deferred)
42
+ good = yield* Fiber.join(fiber)
43
+ good = yield* Latch.await(latch)
44
+ ```
45
+
46
+ In Effect v4, yielding `Ref`, `Deferred`, `Fiber`, and `Latch` directly is removed. Use explicit method calls (`Ref.get`, `Deferred.await`, `Fiber.join`, `Latch.await`) for clarity and forward compatibility.
@@ -0,0 +1,46 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: casting-awareness
6
+ description: Type assertions bypass the compiler — use type-safe alternatives
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern: $A as $B
10
+ level: info
11
+ suggestSkills:
12
+ - effect-domain-modeling
13
+ - effect-domain-predicates
14
+ ---
15
+
16
+ # Stop — do you actually need `as`?
17
+
18
+ Most `as` casts can be replaced. Try these in order:
19
+
20
+ 1. **Remove it.** Check the actual type with LSP (`hover`/`Go to Definition`). If it's already correct or the cast just papers over a fixable upstream type, delete the assertion entirely.
21
+
22
+ 2. **`satisfies`** — validates a value matches a type at compile time without changing the inferred type. No runtime cost, no lying to the compiler:
23
+
24
+ ```ts
25
+ const config = { port: 3000 } satisfies ServerConfig;
26
+ ```
27
+
28
+ 3. **`Schema.is(MySchema)`** — runtime type guard that narrows correctly. Replaces `as` when you need to check unknown/union data:
29
+
30
+ ```ts
31
+ if (Schema.is(User)(value)) {
32
+ /* value: User */
33
+ }
34
+ ```
35
+
36
+ 4. **`Schema.decodeUnknownSync(MySchema)`** — validates unknown data with full error reporting instead of blindly trusting it:
37
+
38
+ ```ts
39
+ const user = Schema.decodeUnknownSync(User)(data);
40
+ ```
41
+
42
+ 5. **`Predicate.isString` / `isNumber` / `isRecord` / etc.** — Effect's built-in type guards for primitives and structures.
43
+
44
+ 6. **`MyEnum.$is("Tag")`** or **`Schema.is(VariantSchema)`** — type guard for discriminated union variants. See `effect-domain-modeling` skill for the full pattern.
45
+
46
+ `as const` is fine — it narrows to literal types. Every other `as` is the compiler waving a white flag. Fix the types instead.
@@ -0,0 +1,84 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: context-tag-extends
6
+ description: Use Context.Service for all service definitions — avoid Context.Tag, Effect.Service, and the legacy ServiceMap.* APIs
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - pattern: 'class $A extends Context.Tag'
12
+ - pattern: 'class $A extends Context.Tag<$$$>() { $$$ }'
13
+ - pattern: 'Context.GenericTag<$$$>'
14
+ - pattern: 'Context.Tag($$$)'
15
+ - pattern: 'Effect.Service<$$$>()'
16
+ - pattern: 'ServiceMap.Service'
17
+ - pattern: 'ServiceMap.Reference'
18
+ - pattern: 'ServiceMap.make'
19
+ - pattern: 'ServiceMap.get'
20
+ - pattern: 'ServiceMap.add'
21
+ - pattern: 'ServiceMap.mergeAll'
22
+ level: warning
23
+ suggestSkills:
24
+ - effect-service-implementation
25
+ ---
26
+
27
+ # Use `Context.Service` for All Service Definitions
28
+
29
+ `Context.Service` is the single canonical service-definition API in Effect v4 (beta.46+). Three legacy spellings exist and must all be replaced:
30
+
31
+ | Legacy API | Era | Replacement |
32
+ | --------------------------- | --------- | -------------------------- |
33
+ | `Context.Tag` / `GenericTag` | pre-v4 | `Context.Service` |
34
+ | `Effect.Service` | early v4 | `Context.Service` |
35
+ | `ServiceMap.Service` / `.*` | beta.43 | `Context.Service` / `Context.*` |
36
+
37
+ ```haskell
38
+ -- Anti-pattern: *Tag suffix + Context.Tag — removed in v4
39
+ class ParallelClientTag extends Context.Tag
40
+ data ParallelClientService = ...
41
+ -- two names for one concept = unnecessary coupling
42
+
43
+ -- Anti-pattern: Effect.Service — also removed in v4
44
+ class ParallelClient extends Effect.Service<ParallelClient>()(...)
45
+
46
+ -- Anti-pattern: ServiceMap.* — removed before v4 beta.46 stabilized
47
+ class MyService extends ServiceMap.Service<MyService>()("@app/MyService", { ... })
48
+
49
+ -- Fix: Context.Service (beta.46 API)
50
+ class ParallelClient extends Context.Service<ParallelClient>()(
51
+ "@parallel/ParallelClient"
52
+ )
53
+ ```
54
+
55
+ ```typescript
56
+ // Concrete service (single implementation)
57
+ export class ParallelClient extends Context.Service<ParallelClient>()(
58
+ '@parallel/ParallelClient'
59
+ ) {}
60
+
61
+ // Interface-style service (multiple implementations, config, infrastructure)
62
+ export class Clipboard extends Context.Service<Clipboard>()(
63
+ '@Clipboard/Clipboard'
64
+ ) {}
65
+ ```
66
+
67
+ When migrating from `ServiceMap`, also update the corresponding module accessors:
68
+
69
+ - `ServiceMap.get` → `Context.get`
70
+ - `ServiceMap.make` → `Context.make`
71
+ - `ServiceMap.add` → `Context.add`
72
+ - `ServiceMap.mergeAll` → `Context.mergeAll`
73
+ - `effect/ServiceMap` import → `effect/Context`
74
+
75
+ ## Design Rules
76
+
77
+ - **Never** use a `*Tag` suffix — name the service directly
78
+ - `Context.Service` gives you service identity and lookup; it does not replace explicit layer design
79
+ - Export `layer` to expose the real dependency graph
80
+ - Export `defaultLayer` only when there are unsatisfied requirements to wire
81
+ - Capture dependencies in `Layer.effect` via `yield*`, not hidden module globals
82
+ - Access services with `yield* MyService` or `MyService.use(...)`
83
+
84
+ This pattern subsumes the legacy `use-context-service` pattern — the `ServiceMap.*` family is now folded in here so there's a single, comprehensive entry point for v4 service-definition migration.
@@ -0,0 +1,61 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: effect-catchall-default
6
+ description: Avoid broad Effect.catch defaults in domain logic - use catchTag unless this is an explicit boundary fallback
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern:
10
+ - 'Effect.catch($X => Effect.succeed($$$))'
11
+ - 'Effect.catch($X => Effect.sync($$$))'
12
+ - 'Effect.catch($X => succeed($$$))'
13
+ - 'Effect.catch($X => sync($$$))'
14
+ level: warning
15
+ suggestSkills:
16
+ - effect-error-handling
17
+ ---
18
+
19
+ # Avoid `Effect.catch` with Default Values
20
+
21
+ ```haskell
22
+ -- Transformation
23
+ catch :: (E -> Effect a) -> Effect a E -> Effect a _
24
+ catch _ default = \_ -> succeed default -- swallows all errors silently
25
+
26
+ -- Instead
27
+ catchTag :: Tag -> (E -> Effect a) -> Effect a E -> Effect a (E - Tag)
28
+ catchTags :: {Tag1: h1, ...} -> Effect a E -> Effect a (E - Tags)
29
+ ```
30
+
31
+ ```haskell
32
+ -- Pattern
33
+ bad :: Effect User _
34
+ bad = pipe
35
+ fetchUser
36
+ $ catch \_ -> succeed defaultUser -- which error? why?
37
+
38
+ good :: Effect User (NetworkError | Timeout)
39
+ good = pipe
40
+ fetchUser
41
+ $ catchTag "NotFound" \_ -> do
42
+ log "User not found, creating..."
43
+ createDefaultUser -- explicit, logged, traceable
44
+
45
+ -- For expected absence
46
+ better :: Effect (Option User) NetworkError
47
+ better = pipe
48
+ fetchUser
49
+ $ Option.some -- Option, not error swallowing
50
+ $ catchTag "NotFound" \_ -> Option.none
51
+ ```
52
+
53
+ `Effect.catch` with defaults often hides bugs and loses context. Use `catchTag` for specific errors with logging, or `Option` for expected absence.
54
+
55
+ Legitimate exceptions exist at explicit boundaries:
56
+
57
+ - best-effort cache hydration
58
+ - capability probes and optional integrations
59
+ - remote config or metadata loads where the contract is "fallback to neutral default"
60
+
61
+ In those cases, document the fallback intent in code review and keep the fallback close to the boundary.
@@ -0,0 +1,47 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: effect-promise-vs-trypromise
6
+ description: Use Effect.tryPromise instead of Effect.promise for error handling
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern: Effect.promise
10
+ level: warning
11
+ suggestSkills:
12
+ - effect-error-handling
13
+ ---
14
+
15
+ # Use Effect.tryPromise Instead of Effect.promise
16
+
17
+ ```haskell
18
+ -- Transformation
19
+ promise :: IO (Promise a) → Effect a ∅ -- rejection = defect (uncatchable)
20
+ tryPromise :: IO (Promise a) → Effect a E -- rejection = typed error (catchable)
21
+ ```
22
+
23
+ ```haskell
24
+ -- Pattern
25
+ bad :: Effect User ∅
26
+ bad = Effect.promise \_ → fetchUser id
27
+ -- rejection becomes Defect: can't catch, crashes fiber
28
+
29
+ good :: Effect User FetchError
30
+ good = Effect.tryPromise
31
+ { try: \_ → fetchUser id
32
+ , catch: \e → FetchError (show e)
33
+ }
34
+ -- rejection becomes typed error: catchable, testable
35
+
36
+ -- Error handling
37
+ handle :: Effect User FetchError → Effect User ∅
38
+ handle = catchTag "FetchError" \e → defaultUser
39
+
40
+ -- Defects bypass all handlers
41
+ defect :: Effect a ∅ → Effect a E
42
+ defect = id -- can't recover from defects
43
+ ```
44
+
45
+ `Effect.promise` converts rejections to uncatchable defects. Use `Effect.tryPromise` for typed, recoverable errors in the E channel.
46
+
47
+ Any reference to `Effect.promise` is flagged — not just `yield* Effect.promise(...)`. Piping, passing, or returning `Effect.promise` propagates the same defect-conversion problem to the consumer and is equally wrong.