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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- 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.
|