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,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).
|