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,83 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: require-effect-concurrency
6
+ description: Specify concurrency explicitly for Effect collection combinators
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - all:
12
+ - pattern:
13
+ context: Effect.forEach($EACH)
14
+ strictness: signature
15
+ - regex: '^Effect\.forEach'
16
+ - all:
17
+ - pattern:
18
+ context: Effect.forEach($A, $OPTIONS)
19
+ strictness: signature
20
+ - regex: '^Effect\.forEach'
21
+ - all:
22
+ - pattern:
23
+ context: Effect.forEach($A, $B, $OPTIONS)
24
+ strictness: signature
25
+ - regex: '^Effect\.forEach'
26
+ - all:
27
+ - pattern:
28
+ context: Effect.all($ALL)
29
+ strictness: signature
30
+ - regex: '^Effect\.all'
31
+ - all:
32
+ - pattern:
33
+ context: Effect.all($A, $OPTIONS)
34
+ strictness: signature
35
+ - regex: '^Effect\.all'
36
+ - all:
37
+ - pattern:
38
+ context: Effect.validate($VALIDATE)
39
+ strictness: signature
40
+ - regex: '^Effect\.validate'
41
+ - all:
42
+ - pattern:
43
+ context: Effect.validate($A, $OPTIONS)
44
+ strictness: signature
45
+ - regex: '^Effect\.validate'
46
+ - all:
47
+ - pattern:
48
+ context: Effect.validate($A, $B, $OPTIONS)
49
+ strictness: signature
50
+ - regex: '^Effect\.validate'
51
+ constraints:
52
+ OPTIONS:
53
+ not:
54
+ all:
55
+ - kind: object
56
+ - has:
57
+ regex: '\bconcurrency\b'
58
+ level: warning
59
+ suggestSkills:
60
+ - effect-concurrency-testing
61
+ ---
62
+
63
+ # Specify Effect Collection Concurrency
64
+
65
+ ```haskell
66
+ -- Transformation
67
+ Effect.forEach xs f -- concurrency intent implicit
68
+ Effect.forEach xs f opts -- concurrency intent explicit
69
+ ```
70
+
71
+ ```typescript
72
+ // Bad
73
+ Effect.forEach(items, processItem);
74
+ Effect.all(tasks);
75
+ Effect.validate(inputs, validateInput, { discard: true });
76
+
77
+ // Good
78
+ Effect.forEach(items, processItem, { concurrency: 1 });
79
+ Effect.all(tasks, { concurrency: 'unbounded' });
80
+ Effect.validate(inputs, validateInput, { concurrency: 4, discard: true });
81
+ ```
82
+
83
+ Even sequential execution is a concurrency decision. Specify `concurrency` on `Effect.forEach`, `Effect.all`, and `Effect.validate` so throughput and ordering intent are reviewable at the call site.
@@ -0,0 +1,63 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: stream-large-files
6
+ description: Review whole-file reads when the path appears large or unbounded
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - pattern: fs.readFile($PATH)
12
+ - pattern: fs.readFileString($PATH)
13
+ constraints:
14
+ PATH:
15
+ any:
16
+ - regex: '(large|huge|dump|archive|dataset|backup|export|log|logs|jsonl|ndjson|csv)'
17
+ - pattern: $DIR + $FILE
18
+ - pattern: path.join($$$)
19
+ - pattern: path.resolve($$$)
20
+ - pattern: $FILES[$I]
21
+ - pattern: $ITEM.path
22
+ level: info
23
+ suggestSkills:
24
+ - effect-stream
25
+ ---
26
+
27
+ # Consider Streaming Large Files
28
+
29
+ ```haskell
30
+ -- Transformation
31
+ readFile :: FilePath → Effect String FileSystem -- entire file in memory
32
+ stream :: FilePath → Stream Chunk FileSystem -- incremental chunks
33
+
34
+ -- For large files
35
+ fs.stream path { chunkSize: 64 * 1024 }
36
+ |> decodeText "utf-8"
37
+ |> splitLines
38
+ |> map processLine
39
+ |> runDrain
40
+ ```
41
+
42
+ ```haskell
43
+ -- Pattern
44
+ bad :: FilePath → Effect String FileSystem
45
+ bad path = readFileString path -- OOM on gigabyte files
46
+
47
+ good :: FilePath → Effect () FileSystem
48
+ good path = pipe
49
+ (fs.stream path { chunkSize: 65536 })
50
+ $ Stream.decodeText "utf-8"
51
+ $ Stream.splitLines
52
+ $ Stream.map processLine
53
+ $ Stream.runDrain -- constant memory usage
54
+
55
+ -- When to stream
56
+ shouldStream :: FileSize → Bool
57
+ shouldStream size
58
+ | size > megabytes 100 = True -- definitely stream
59
+ | lineByLine needed = True -- stream for efficiency
60
+ | otherwise = False -- readFile is fine
61
+ ```
62
+
63
+ `readFile` loads entire file into memory. Use `fs.stream` for large files or line-by-line processing to avoid OOM errors.
@@ -0,0 +1,62 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: throw-in-effect-gen
6
+ description: Do not throw inside Effect.gen / Effect.fn / Effect.fnUntraced — use yield* Effect.fail() instead. The `try:` callback of Effect.try / Effect.tryPromise is exempt.
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ pattern: throw $ERR
11
+ inside:
12
+ any:
13
+ - pattern: Effect.gen($$$ARGS)
14
+ - pattern: Effect.fn($$$ARGS)
15
+ - pattern: Effect.fn($$$ARGS)($$$BODY)
16
+ - pattern: Effect.fnUntraced($$$ARGS)
17
+ stopBy: end
18
+ not:
19
+ inside:
20
+ any:
21
+ - pattern: 'Effect.try({ try: $$$ })'
22
+ - pattern: 'Effect.tryPromise({ try: $$$ })'
23
+ stopBy: end
24
+ level: critical
25
+ suggestSkills:
26
+ - effect-error-handling
27
+ ---
28
+
29
+ # Do Not `throw` Inside `Effect.gen`
30
+
31
+ ```haskell
32
+ -- Transformation
33
+ throw :: Error -> ⊥ -- untyped, uncatchable by Effect
34
+ yield* Effect.fail :: TaggedError -> E ⊥ E -- typed, catchable via catchTag
35
+ ```
36
+
37
+ ```haskell
38
+ -- Pattern
39
+ bad :: Effect.gen
40
+ bad = Effect.gen \_ -> do
41
+ throw new Error("not found") -- bypasses Effect error channel
42
+
43
+ good :: Effect.gen
44
+ good = Effect.gen \_ -> do
45
+ yield* Effect.fail(new UserNotFoundError({ message: "not found" }))
46
+ -- typed error, catchable with catchTag("UserNotFoundError", ...)
47
+ ```
48
+
49
+ `throw` inside `Effect.gen` / `Effect.fn` / `Effect.fnUntraced` creates a defect (untyped), not a typed error. Use `yield* Effect.fail(new SchemaTaggedError(...))` to keep errors in the typed channel.
50
+
51
+ ## Exemption: the `try:` callback of `Effect.try` / `Effect.tryPromise`
52
+
53
+ Throwing inside the `try:` callback of `Effect.try({ try, catch })` or `Effect.tryPromise({ try, catch })` is the *intended* shape — the paired `catch:` handler captures the throwable back into the typed error channel:
54
+
55
+ ```typescript
56
+ Effect.tryPromise({
57
+ try: () => fetch(url), // ✓ throws here are caught by `catch` below
58
+ catch: (cause) => new FetchError({ url, message: String(cause) })
59
+ });
60
+ ```
61
+
62
+ The rule's `not.inside` clause carves out exactly that shape so legitimate `Effect.try` / `Effect.tryPromise` callbacks don't fire the diagnostic. Throws anywhere else inside an `Effect.gen` / `Effect.fn` / `Effect.fnUntraced` body remain flagged.
@@ -0,0 +1,45 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: use-clock-service
6
+ description: Use Effect DateTime instead of JS Date
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern:
10
+ - 'new Date($$$)'
11
+ - 'Date.$M($$$)'
12
+ level: warning
13
+ suggestSkills:
14
+ - effect-testing
15
+ ---
16
+
17
+ # Use Effect DateTime Instead of JS Date
18
+
19
+ ```haskell
20
+ -- Transformation
21
+ newDate :: IO Date -- impure, non-deterministic
22
+ dateNow :: IO Milliseconds -- side effect, untestable
23
+
24
+ -- Instead
25
+ now :: Effect DateTime R -- R includes Clock
26
+ currentMs :: Effect Millis Clock -- explicit dependency
27
+ ```
28
+
29
+ ```haskell
30
+ -- Pattern
31
+ bad :: IO Timestamp
32
+ bad = Date.now -- where R = ∅, untestable
33
+
34
+ good :: Effect Timestamp Clock
35
+ good = Clock.currentTimeMillis -- where R ⊃ Clock, testable
36
+
37
+ -- In tests
38
+ test :: Effect () TestClock
39
+ test = do
40
+ TestClock.adjust (minutes 5) -- deterministic time
41
+ result ← good
42
+ assert (result == expected)
43
+ ```
44
+
45
+ Direct `Date` usage is non-deterministic. Use `DateTime.now` or `Clock.currentTimeMillis` for testable time operations via `TestClock`.
@@ -0,0 +1,54 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: use-command-executor-service
6
+ description: Use Effect's ChildProcessSpawner instead of node:child_process
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - all:
12
+ - kind: import_statement
13
+ - regex: '["''](?:node:)?child_process["'']'
14
+ - pattern: require($SPEC)
15
+ - pattern: import($SPEC)
16
+ constraints:
17
+ SPEC:
18
+ regex: '^["''](?:node:)?child_process["'']$'
19
+ level: warning
20
+ suggestSkills:
21
+ - effect-command-executor
22
+ - effect-platform-abstraction
23
+ ---
24
+
25
+ # Use Effect Process Services Instead of `child_process`
26
+
27
+ ```haskell
28
+ -- Transformation
29
+ import "node:child_process" :: Node → IO Process -- callback-spaghetti, untyped
30
+ spawn / exec / execFile :: Args → Promise / Stream -- ad-hoc lifecycle
31
+
32
+ -- Instead
33
+ ChildProcess :: Args → ChildProcess -- pure value, declarative
34
+ ChildProcessSpawner :: Effect a ChildProcessSpawner -- typed I/O, scoped lifetime
35
+ ```
36
+
37
+ ```haskell
38
+ -- Pattern (Effect v4 — effect/unstable/process)
39
+ bad :: () → IO String
40
+ bad = exec "git" ["status"] (cb) -- callback, untyped, no cancellation
41
+
42
+ good :: Effect String ChildProcessSpawner
43
+ good = do
44
+ spawner ← ChildProcessSpawner.ChildProcessSpawner
45
+ spawner.string (ChildProcess.make "git" ["status"])
46
+ -- typed output, error channel, scoped lifetime
47
+ ```
48
+
49
+ Direct `child_process` imports give you callback APIs, manual lifecycle, and no error channel. Use `ChildProcessSpawner` and `ChildProcess` from `effect/unstable/process` for typed errors, scoped resource lifetime, and platform-agnostic process spawning.
50
+
51
+ **Exceptions:**
52
+
53
+ - Build scripts and tooling config that legitimately couple to Node
54
+ - Platform-specific layers that implement `ChildProcessSpawner`
@@ -0,0 +1,54 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: use-console-service
6
+ description: Use Effect Console or Effect.log instead of console
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern: console.$M($$$)
10
+ level: warning
11
+ suggestSkills:
12
+ - effect-observability
13
+ ---
14
+
15
+ # Use Effect Console Instead of console.\*
16
+
17
+ ```haskell
18
+ -- Transformation
19
+ console.log :: String → IO () -- side effect, not composable
20
+ console.error :: String → IO () -- same problem
21
+
22
+ -- Instead
23
+ Console.log :: String → Effect () Console
24
+ Effect.log :: String → Effect () ∅ -- with structured logging
25
+ Effect.logError :: String → Effect () ∅
26
+ ```
27
+
28
+ ```haskell
29
+ -- Pattern
30
+ bad :: Effect ()
31
+ bad = do
32
+ Effect.sync \_ → console.error "Error:" error -- ceremony with no benefit
33
+
34
+ good :: Effect ()
35
+ good = Console.error ("Error:" <> show error) -- proper Effect console
36
+
37
+ better :: Effect ()
38
+ better = Effect.logError error -- structured, with context
39
+
40
+ -- Why Effect logging
41
+ structured :: Effect () ∅
42
+ structured = do
43
+ Effect.logInfo "Processing" `withLogSpan` "request"
44
+ -- adds: timestamp, span, log level, structured context
45
+
46
+ -- Testable
47
+ test :: Effect () TestConsole
48
+ test = do
49
+ program
50
+ logs ← TestConsole.output
51
+ assert (logs `contains` "expected message")
52
+ ```
53
+
54
+ `console.*` in Effect code breaks the paradigm. Use `Console` service or `Effect.log*` for structured, testable logging.
@@ -0,0 +1,59 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: use-filesystem-service
6
+ description: Use FileSystem service instead of direct Node.js fs / fs/promises imports
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - all:
12
+ - kind: import_statement
13
+ - regex: '["''](?:node:)?fs(?:/promises)?["'']'
14
+ - pattern: require($SPEC)
15
+ - pattern: import($SPEC)
16
+ constraints:
17
+ SPEC:
18
+ regex: '^["''](?:node:)?fs(?:/promises)?["'']$'
19
+ level: high
20
+ suggestSkills:
21
+ - effect-filesystem
22
+ ---
23
+
24
+ # Use FileSystem Service Instead of `fs` / `fs/promises`
25
+
26
+ ```haskell
27
+ -- Transformation
28
+ import "node:fs" :: Node → IO a -- platform-coupled, untestable
29
+ import "fs" :: Node → IO a -- same problem
30
+ import "node:fs/promises" :: Node → Promise a -- Promise-based, not Effect
31
+ import "fs/promises" :: Node → Promise a -- same problem
32
+
33
+ -- Instead
34
+ FileSystem :: Effect a FileSystem -- platform-agnostic, Effect-native
35
+ ```
36
+
37
+ ```haskell
38
+ -- Pattern
39
+ bad :: FilePath → IO String
40
+ bad path = fs.readFileSync path "utf-8" -- R = Node, untestable, blocks event loop
41
+
42
+ bad₂ :: FilePath → Promise String
43
+ bad₂ path = fsPromises.readFile path "utf-8" -- Promise, not Effect
44
+
45
+ good :: FilePath → Effect String FileSystem
46
+ good path = do
47
+ fs ← FileSystem.FileSystem
48
+ fs.readFileString path -- R ⊃ FileSystem, portable, typed errors
49
+
50
+ -- Platform provision at entry point
51
+ main :: Effect () (FileSystem | Console | ...)
52
+ main = program
53
+ & provide BunServices.layer -- or NodeServices.layer
54
+ & runMain
55
+ ```
56
+
57
+ Direct `fs` / `fs/promises` imports couple code to Node.js and bypass the Effect error channel. Use `FileSystem` from `effect` for portability across Node and Bun, with typed errors and composable I/O; provide the concrete platform layer only at the runtime boundary.
58
+
59
+ This pattern subsumes the legacy `avoid-fs-promises` pattern — both `fs` and `fs/promises` are covered here.
@@ -0,0 +1,77 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: use-http-client-service
6
+ description: Use Effect HttpClient instead of node:http / node:https
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - all:
12
+ - kind: import_statement
13
+ - regex: '["''](?:node:)?https?["'']'
14
+ - pattern: require($SPEC)
15
+ - pattern: import($SPEC)
16
+ constraints:
17
+ SPEC:
18
+ regex: '^["''](?:node:)?https?["'']$'
19
+ level: warning
20
+ suggestSkills:
21
+ - effect-http-client
22
+ - effect-platform-abstraction
23
+ ---
24
+
25
+ # Use Effect HttpClient Instead of `node:http` / `node:https`
26
+
27
+ ```haskell
28
+ -- Transformation
29
+ import "node:http" :: Node → IO Request -- low-level, callback-based
30
+ import "node:https" :: Node → IO Request -- same problem with TLS
31
+
32
+ -- Instead
33
+ HttpClient :: Request → Effect Response -- typed, composable, testable
34
+ HttpClientRequest :: Request builder -- pure value, no side effects
35
+ HttpClientResponse :: Response codec helpers -- schema-validated JSON, streams, etc.
36
+ ```
37
+
38
+ ```haskell
39
+ -- Pattern
40
+ bad :: URL → IO Response
41
+ bad url = http.get url (cb) -- callback, untyped errors
42
+
43
+ good :: URL → Effect User HttpClient
44
+ good url = pipe
45
+ (HttpClient.get url)
46
+ (Effect.flatMap (HttpClientResponse.schemaBodyJson User))
47
+ -- schema-decoded body, typed error channel, layer-based testing
48
+ ```
49
+
50
+ ```haskell
51
+ -- Composable request building
52
+ request :: Effect HttpClientRequest HttpBodyError
53
+ request = pipe
54
+ (HttpClientRequest.post "/api/users")
55
+ (HttpClientRequest.bodyJson { name: "Alice" })
56
+
57
+ -- With retry, timeout, tracing
58
+ resilient :: Effect Response (HttpClient | HttpBodyError)
59
+ resilient = pipe
60
+ request
61
+ (Effect.flatMap HttpClient.execute)
62
+ (Effect.retry (Schedule.recurs 3))
63
+ (Effect.timeout (Duration.seconds 10))
64
+
65
+ -- Provide platform layer at entry point
66
+ main = program
67
+ & provide BunHttpClient.layer -- or NodeHttpClient.layer
68
+ ```
69
+
70
+ Direct `http` / `https` imports give you callback APIs, manual TLS plumbing, and no error channel. Use Effect's `HttpClient`, `HttpClientRequest`, and `HttpClientResponse` for typed errors, composable request building, declarative retry/timeout, and testability via layer substitution.
71
+
72
+ **Exceptions:**
73
+
74
+ - Platform-specific layers that implement `HttpClient.HttpClient`
75
+ - Build scripts and tooling that legitimately need raw Node HTTP
76
+
77
+ References: EF-9b in effect-first-development.md
@@ -0,0 +1,53 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: use-path-service
6
+ description: Use Path service instead of direct Node.js path imports
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - all:
12
+ - kind: import_statement
13
+ - regex: '["''](?:node:)?path["'']'
14
+ - pattern: require($SPEC)
15
+ - pattern: import($SPEC)
16
+ constraints:
17
+ SPEC:
18
+ regex: '^["''](?:node:)?path["'']$'
19
+ level: warning
20
+ suggestSkills:
21
+ - effect-path
22
+ ---
23
+
24
+ # Use Path Service Instead of `path`
25
+
26
+ ```haskell
27
+ -- Transformation
28
+ import "node:path" :: Node → Path a -- platform-coupled
29
+ import "path" :: Node → Path a -- same problem
30
+
31
+ -- Instead
32
+ Path :: Effect Path Path -- platform-agnostic
33
+ ```
34
+
35
+ ```haskell
36
+ -- Pattern
37
+ bad :: String → String → String
38
+ bad dir file = path.join dir file -- R = Node
39
+
40
+ good :: String → String → Effect String Path
41
+ good dir file = do
42
+ p ← Path.Path
43
+ p.join dir file -- R ⊃ Path, portable
44
+
45
+ -- Path operations
46
+ join :: [String] → Effect String Path
47
+ dirname :: String → Effect String Path
48
+ basename :: String → Effect String Path
49
+ extname :: String → Effect String Path
50
+ resolve :: String → Effect String Path
51
+ ```
52
+
53
+ Direct `path` imports couple code to Node.js. Use `Path` from `effect` for cross-platform path operations and provide the concrete platform layer at the runtime boundary.
@@ -0,0 +1,45 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: use-random-service
6
+ description: Use Random service instead of Math.random()
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern: Math.random()
10
+ level: warning
11
+ suggestSkills:
12
+ - effect-testing
13
+ ---
14
+
15
+ # Use Random Service Instead of `Math.random()`
16
+
17
+ ```haskell
18
+ -- Transformation
19
+ mathRandom :: IO Float -- impure, non-deterministic
20
+ random :: Effect Float Random -- explicit dependency, testable
21
+
22
+ -- Random operations
23
+ next :: Effect Float Random
24
+ nextInt :: Effect Int Random
25
+ nextRange :: (Int, Int) → Effect Int Random
26
+ shuffle :: [a] → Effect [a] Random
27
+ ```
28
+
29
+ ```haskell
30
+ -- Pattern
31
+ bad :: IO Int
32
+ bad = floor (Math.random * 100) -- R = ∅, untestable
33
+
34
+ good :: Effect Int Random
35
+ good = Random.nextIntBetween 0 100 -- R ⊃ Random, deterministic in tests
36
+
37
+ -- In tests
38
+ test :: Effect () TestRandom
39
+ test = do
40
+ TestRandom.feedInts [42, 7, 13] -- deterministic sequence
41
+ result ← good
42
+ assert (result == 42)
43
+ ```
44
+
45
+ `Math.random()` is non-deterministic. Use `Random` service for reproducible randomness via `TestRandom.feed*` in tests.
@@ -0,0 +1,66 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: use-temp-file-scoped
6
+ description: Use makeTempFileScoped/makeTempDirectoryScoped instead of os.tmpdir() or non-scoped variants
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - all:
12
+ - kind: import_statement
13
+ - regex: '["''](?:node:)?os["'']'
14
+ - pattern: require($SPEC)
15
+ - pattern: import($SPEC)
16
+ - pattern: os.tmpdir()
17
+ - pattern: $FS.makeTempFile($$$)
18
+ - pattern: $FS.makeTempDirectory($$$)
19
+ constraints:
20
+ SPEC:
21
+ regex: '^["''](?:node:)?os["'']$'
22
+ level: warning
23
+ suggestSkills:
24
+ - effect-filesystem
25
+ ---
26
+
27
+ # Use Scoped Temp Files for Automatic Cleanup
28
+
29
+ ```haskell
30
+ -- Transformation
31
+ os.tmpdir :: IO FilePath -- Node-coupled, manual cleanup
32
+ makeTempFile :: Effect FilePath FS -- manual cleanup required
33
+ makeTempFileScoped :: Effect FilePath (FS | Scope) -- auto cleanup
34
+
35
+ -- Scoped resources
36
+ withTempFile :: (FilePath → Effect a) → Effect a
37
+ withTempFile use = scoped $ do
38
+ path ← makeTempFileScoped { prefix: "myapp-" }
39
+ use path
40
+ -- auto cleanup on scope exit, even on error
41
+ ```
42
+
43
+ ```haskell
44
+ -- Pattern
45
+ bad :: Effect () FS
46
+ bad = do
47
+ tmpFile ← makeTempFile
48
+ writeFileString tmpFile "data"
49
+ remove tmpFile -- might not run on error!
50
+
51
+ good :: Effect () (FS | Scope)
52
+ good = scoped $ do
53
+ tmpFile ← makeTempFileScoped { prefix: "myapp-" }
54
+ writeFileString tmpFile "data"
55
+ -- auto removed when scope ends
56
+
57
+ -- Directories too
58
+ withTempDir :: Effect () (FS | Scope)
59
+ withTempDir = scoped $ do
60
+ dir ← makeTempDirectoryScoped { prefix: "myapp-" }
61
+ path ← join dir "file.txt"
62
+ writeFileString path "data"
63
+ -- entire dir removed when scope ends
64
+ ```
65
+
66
+ `makeTempFileScoped` provides automatic cleanup via Effect's scope. Never use `os.tmpdir()` (Node-coupled) or unscoped variants (leak resources).