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
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://www.schemastore.org/package.json",
|
|
3
|
+
"name": "opencode-effect-enforcer",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "OpenCode V2 plugin for Effect v4 skills, guidance, and pattern enforcement",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"opencode",
|
|
8
|
+
"opencode-plugin",
|
|
9
|
+
"effect",
|
|
10
|
+
"effect-ts"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/mpsuesser/opencode-effect-enforcer#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/mpsuesser/opencode-effect-enforcer/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Marc Suesser",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/mpsuesser/opencode-effect-enforcer.git"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": "./src/index.ts",
|
|
27
|
+
"files": [
|
|
28
|
+
"src",
|
|
29
|
+
"skills",
|
|
30
|
+
"guidance",
|
|
31
|
+
"patterns",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"fmt": "dprint fmt",
|
|
37
|
+
"fmt:check": "dprint check",
|
|
38
|
+
"lint": "oxlint -c oxlintrc.json",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"check": "bun run fmt:check && bun run lint && bun run typecheck",
|
|
42
|
+
"prepublishOnly": "bun run check && bun run test"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@ast-grep/napi": "^0.42.3",
|
|
46
|
+
"@opencode-ai/plugin": "0.0.0-next-17148",
|
|
47
|
+
"diff": "^9.0.0",
|
|
48
|
+
"effect": "4.0.0-rc.111",
|
|
49
|
+
"picomatch": "^4.0.3",
|
|
50
|
+
"yaml": "^2.8.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "^1.3.0",
|
|
54
|
+
"@types/diff": "^8.0.0",
|
|
55
|
+
"@types/json-schema": "^7.0.15",
|
|
56
|
+
"@types/picomatch": "^4.0.2",
|
|
57
|
+
"dprint": "^0.50.2",
|
|
58
|
+
"oxlint": "^1.12.0",
|
|
59
|
+
"typescript": "^5.9.2",
|
|
60
|
+
"vitest": "^3.2.4"
|
|
61
|
+
},
|
|
62
|
+
"packageManager": "bun@1.3.14"
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-any
|
|
6
|
+
description: Avoid using 'as any' or 'as unknown as' type assertions
|
|
7
|
+
glob: '**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
pattern:
|
|
10
|
+
- '$A as any'
|
|
11
|
+
- '$A as unknown'
|
|
12
|
+
level: warning
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Avoid `as any` Type Assertions
|
|
16
|
+
|
|
17
|
+
```haskell
|
|
18
|
+
-- Transformation
|
|
19
|
+
asAny :: a → Any -- erases type, defeats compiler
|
|
20
|
+
asUnknown :: a → Unknown -- same problem with extra steps
|
|
21
|
+
|
|
22
|
+
-- Instead
|
|
23
|
+
decode :: Schema a → Unknown → Either ParseError a
|
|
24
|
+
guard :: (a → Bool) → a → Maybe a
|
|
25
|
+
generics :: ∀ a. Constraint a ⇒ a → F a
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```haskell
|
|
29
|
+
-- Pattern
|
|
30
|
+
bad :: Unknown → T
|
|
31
|
+
bad x = x `as` T -- trust me bro
|
|
32
|
+
|
|
33
|
+
good :: Unknown → Either ParseError T
|
|
34
|
+
good x = Schema.decode schemaT x -- prove it
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Using `as any` bypasses type checking entirely. The `as unknown as T` pattern is equivalent—casting through `unknown` still erases type information. Note: `as const` is acceptable—it narrows to literal types without erasing type safety.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-data-tagged-error
|
|
6
|
+
description: Review Data.TaggedError usage; public or serialized errors should use Schema.TaggedError
|
|
7
|
+
glob: '**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
pattern: Data.TaggedError($$$)
|
|
10
|
+
level: info
|
|
11
|
+
suggestSkills:
|
|
12
|
+
- effect-error-handling
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Prefer `Schema.TaggedError` for Public Errors
|
|
16
|
+
|
|
17
|
+
```haskell
|
|
18
|
+
-- Transformation
|
|
19
|
+
Data.TaggedError :: String -> { fields } -> Error -- lightweight, not schema-backed
|
|
20
|
+
Schema.TaggedError :: String -> { schemas } -> Error -- serializable, RPC-ready
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```haskell
|
|
24
|
+
-- Pattern
|
|
25
|
+
bad :: Error
|
|
26
|
+
bad = class MyError extends Data.TaggedError("MyError")<{ message: string }>
|
|
27
|
+
|
|
28
|
+
good :: Error
|
|
29
|
+
good = class MyError extends Schema.TaggedError<MyError>()("MyError", {
|
|
30
|
+
message: Schema.String
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`Schema.TaggedError` provides serialization, RPC compatibility, and runtime validation. Prefer it for public, cross-module, persisted, or wire-visible failures. `Data.TaggedError` remains valid for lightweight module-internal errors that do not need schema encoding, so treat this finding as a design review rather than an automatic rewrite.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-direct-json
|
|
6
|
+
description: Consider using Schema.fromJsonString instead of direct JSON methods
|
|
7
|
+
glob: '**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
pattern: JSON.$M($$$)
|
|
10
|
+
level: info
|
|
11
|
+
suggestSkills:
|
|
12
|
+
- effect-schema-composition
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Consider Schema JSON Codecs Instead of JSON Methods
|
|
16
|
+
|
|
17
|
+
```haskell
|
|
18
|
+
-- Transformation
|
|
19
|
+
jsonParse :: String → Any -- returns Any, can throw
|
|
20
|
+
jsonStringify :: a → String -- no validation
|
|
21
|
+
|
|
22
|
+
-- Instead
|
|
23
|
+
fromJsonString :: Schema a → Schema String a
|
|
24
|
+
unknownJson :: Schema String unknown
|
|
25
|
+
encodeJson :: Schema a → a → String
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```haskell
|
|
29
|
+
-- Pattern
|
|
30
|
+
bad :: String → IO User
|
|
31
|
+
bad json = JSON.parse json -- returns Any, throws on invalid
|
|
32
|
+
|
|
33
|
+
good :: String → Either ParseError User
|
|
34
|
+
good json = Schema.decodeUnknownSync(Schema.fromJsonString(User)) json
|
|
35
|
+
|
|
36
|
+
unknownJson = Schema.fromJsonString(Schema.Unknown)
|
|
37
|
+
|
|
38
|
+
-- Bidirectional
|
|
39
|
+
data User = Schema.Class "User"
|
|
40
|
+
{ id :: Schema.Number
|
|
41
|
+
, name :: Schema.String
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
decode :: String → Either ParseError User
|
|
45
|
+
decode = Schema.decodeUnknownSync(Schema.fromJsonString(User))
|
|
46
|
+
|
|
47
|
+
encode :: User → String
|
|
48
|
+
encode = Schema.encodeSync(Schema.fromJsonString(User))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`JSON.parse` returns `any` and throws on invalid input. `Schema.fromJsonString(...)` provides typed, validated JSON parsing and encoding. Use `Schema.fromJsonString(Schema.Unknown)` when the JSON shape is intentionally unknown; `Schema.UnknownFromJsonString` is internal in current Effect v4. Direct JSON methods remain reasonable at narrow logging/debugging boundaries.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-direct-tag-checks
|
|
6
|
+
description: Avoid direct _tag property checks; use exported refinements/predicates
|
|
7
|
+
glob: '**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
rule:
|
|
10
|
+
any:
|
|
11
|
+
- pattern: $A._tag === $B
|
|
12
|
+
- pattern: $A._tag !== $B
|
|
13
|
+
- pattern: switch ($A._tag) { $$$ }
|
|
14
|
+
level: warning
|
|
15
|
+
suggestSkills:
|
|
16
|
+
- effect-pattern-matching
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Avoid Direct `_tag` Property Checks
|
|
20
|
+
|
|
21
|
+
```haskell
|
|
22
|
+
-- Transformation
|
|
23
|
+
directCheck :: Event → Bool
|
|
24
|
+
directCheck e = e._tag == "FactRecorded" -- fragile, poor narrowing
|
|
25
|
+
|
|
26
|
+
-- Instead
|
|
27
|
+
$is :: Tag → Event → Bool -- from TaggedEnum
|
|
28
|
+
$match :: { Tag₁: h₁, Tag₂: h₂ } → Event → a -- exhaustive matching
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```haskell
|
|
32
|
+
-- Pattern
|
|
33
|
+
bad :: Event → Effect ()
|
|
34
|
+
bad e
|
|
35
|
+
| e._tag == "FactRecorded" = handleFact e -- manual check, fragile
|
|
36
|
+
| otherwise = pure ()
|
|
37
|
+
|
|
38
|
+
good :: Event → Effect ()
|
|
39
|
+
good = $match
|
|
40
|
+
{ FactRecorded: handleFact
|
|
41
|
+
, QuestionAsked: handleQuestion
|
|
42
|
+
} -- exhaustive, type-safe
|
|
43
|
+
|
|
44
|
+
-- Or with predicates
|
|
45
|
+
data Event = FactRecorded | QuestionAsked
|
|
46
|
+
deriving TaggedEnum
|
|
47
|
+
|
|
48
|
+
isFactRecorded :: Event → Bool
|
|
49
|
+
isFactRecorded = $is "FactRecorded"
|
|
50
|
+
|
|
51
|
+
-- Refactoring-safe: rename tag in one place
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Direct `_tag` checks don't narrow types correctly. Use `$is` for predicates or `$match` for exhaustive pattern matching via `Data.taggedEnum`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-expect-in-if
|
|
6
|
+
description: Avoid nesting expect() calls inside if blocks in tests
|
|
7
|
+
glob: '**/*.{test,spec}.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
rule:
|
|
10
|
+
pattern: expect($$$)
|
|
11
|
+
inside:
|
|
12
|
+
pattern: if ($$$) { $$$ }
|
|
13
|
+
stopBy: end
|
|
14
|
+
level: warning
|
|
15
|
+
suggestSkills:
|
|
16
|
+
- effect-testing
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Avoid `expect()` Inside `if` Blocks
|
|
20
|
+
|
|
21
|
+
```haskell
|
|
22
|
+
-- Anti-pattern
|
|
23
|
+
testBad :: Effect ()
|
|
24
|
+
testBad = do
|
|
25
|
+
result ← runTest
|
|
26
|
+
if isJust (value result) -- condition false → silently passes!
|
|
27
|
+
then expect (name $ fromJust $ value result) `toBe` "test"
|
|
28
|
+
else pure () -- hidden skip
|
|
29
|
+
|
|
30
|
+
-- Pattern
|
|
31
|
+
narrow :: Maybe a → Effect a
|
|
32
|
+
narrow = assert "Expected value to be defined"
|
|
33
|
+
|
|
34
|
+
testGood :: Effect ()
|
|
35
|
+
testGood = do
|
|
36
|
+
result ← runTest
|
|
37
|
+
value ← narrow (value result) -- fails fast if Nothing
|
|
38
|
+
expect (name value) `toBe` "test" -- type narrowed, safe
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```haskell
|
|
42
|
+
-- Transformation
|
|
43
|
+
if value then expect(value.x) else skip -- ✗ silent skip
|
|
44
|
+
assert value; expect(value.x) -- ✓ fail fast
|
|
45
|
+
|
|
46
|
+
-- Alternative
|
|
47
|
+
expect(value) `toBeDefined`
|
|
48
|
+
assert value -- narrow for TS
|
|
49
|
+
expect(value.name) `toBe` "test"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`if (x) { expect(x.y) }` silently passes when condition is false. Use `assert` to narrow types and fail fast.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-mutable-state
|
|
6
|
+
description: Prefer Ref over let bindings for mutable state in Effect services
|
|
7
|
+
glob: '**/*.ts'
|
|
8
|
+
detector: ast
|
|
9
|
+
rule:
|
|
10
|
+
pattern: let $$$DECLS
|
|
11
|
+
inside:
|
|
12
|
+
pattern: Layer.effect($$$)
|
|
13
|
+
stopBy: end
|
|
14
|
+
level: info
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Prefer `Ref` Over `let` for Mutable State
|
|
18
|
+
|
|
19
|
+
```haskell
|
|
20
|
+
-- Transformation
|
|
21
|
+
let x = v; x = v' :: mutable binding -- not fiber-safe
|
|
22
|
+
Ref.make(v) >>= Ref.set :: Ref a -> Effect a -- fiber-safe, composable
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```haskell
|
|
26
|
+
-- Pattern
|
|
27
|
+
bad :: Mutable let
|
|
28
|
+
bad = let counter = 0
|
|
29
|
+
bad = counter += 1
|
|
30
|
+
|
|
31
|
+
good :: Ref
|
|
32
|
+
good = const counterRef = yield* Ref.make(0)
|
|
33
|
+
good = yield* Ref.update(counterRef, (n) => n + 1)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Shared mutable `let` bindings in Effect services are suspicious because they can hide fiber-visible state and lifecycle behavior. Prefer `Ref`, `SynchronizedRef`, or `Effect.cached` for state that is read or written through your service API.
|
|
37
|
+
|
|
38
|
+
## When `let` Is Acceptable
|
|
39
|
+
|
|
40
|
+
- Loop counters in non-effectful pure functions
|
|
41
|
+
- Local temporaries in synchronous helpers
|
|
42
|
+
- Destructuring reassignment in narrow scopes
|
|
43
|
+
|
|
44
|
+
The `info` level reflects that `let` has legitimate uses — this pattern surfaces it for review, not as an error.
|
|
45
|
+
|
|
46
|
+
## Scoped Mutable Collections Can Be Fine
|
|
47
|
+
|
|
48
|
+
Small mutable collections inside `Layer.effect` or `InstanceState.make` are acceptable when all of the following are true:
|
|
49
|
+
|
|
50
|
+
- the collection is private to the service implementation
|
|
51
|
+
- its lifetime is tied to the layer or instance scope
|
|
52
|
+
- callers only observe it through effectful service methods
|
|
53
|
+
- cleanup/finalization is handled explicitly
|
|
54
|
+
|
|
55
|
+
Examples include private `Map` values for registries, PubSub channel lookup tables, or runner maps inside a dedicated coordinator service.
|
|
56
|
+
|
|
57
|
+
## When `Effect.cached` Replaces `let`
|
|
58
|
+
|
|
59
|
+
Mutable fields used for deduplication or caching (`task?: Promise<T>`, `fiber?: Fiber<T>`, `result?: T`) should be replaced with `Effect.cached`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// Before: mutable dedup tracking
|
|
63
|
+
let task: Promise<Result> | undefined;
|
|
64
|
+
const getResult = () => (task ??= computeExpensive());
|
|
65
|
+
|
|
66
|
+
// After: Effect.cached inside service make block
|
|
67
|
+
const cachedResult = yield* Effect.cached(computeExpensive());
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For invalidatable caches, use `Effect.cachedInvalidateWithTTL(effect, Duration.infinity)` instead of rebinding a `let`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-native-fetch
|
|
6
|
+
description: Use Effect HTTP modules instead of native fetch
|
|
7
|
+
glob: '**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
pattern: fetch($$$ARGS)
|
|
10
|
+
level: warning
|
|
11
|
+
suggestSkills:
|
|
12
|
+
- effect-http-client
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Use Effect HTTP Modules Instead of Native `fetch`
|
|
16
|
+
|
|
17
|
+
```haskell
|
|
18
|
+
-- Transformation
|
|
19
|
+
fetch :: String -> Promise Response -- global side effect, untyped errors
|
|
20
|
+
HttpClient :: Request -> Effect Response E R -- typed, composable, testable
|
|
21
|
+
|
|
22
|
+
-- Pattern
|
|
23
|
+
bad :: String -> Promise Response
|
|
24
|
+
bad url = fetch(url) -- untyped rejection, no retry/timeout
|
|
25
|
+
bad url = fetch(url, { method: "POST" }) -- scattered options
|
|
26
|
+
|
|
27
|
+
good :: String -> Effect User HttpError HttpClient
|
|
28
|
+
good url = pipe(
|
|
29
|
+
HttpClient.get(url),
|
|
30
|
+
Effect.flatMap(HttpClientResponse.schemaBodyJson(User))
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```haskell
|
|
35
|
+
-- Composable request building
|
|
36
|
+
request :: Effect HttpClientRequest HttpBodyError
|
|
37
|
+
request = pipe(
|
|
38
|
+
HttpClientRequest.post("/api/users"),
|
|
39
|
+
HttpClientRequest.bodyJson({ name: "Alice" })
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
-- With retry, timeout, tracing
|
|
43
|
+
resilient :: Effect Response HttpError (HttpClient | Scope)
|
|
44
|
+
resilient = pipe(
|
|
45
|
+
request,
|
|
46
|
+
Effect.flatMap(HttpClient.execute),
|
|
47
|
+
Effect.retry(Schedule.recurs(3)),
|
|
48
|
+
Effect.timeout(Duration.seconds(10))
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
-- Platform layer at entry point
|
|
52
|
+
main = program.pipe(
|
|
53
|
+
Effect.provide(BunHttpClient.layer) -- or NodeHttpClient.layer
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Native `fetch` produces untyped Promise rejections. Use `HttpClientRequest`, `HttpClientResponse`, and `HttpClient` from Effect for typed errors, composable request building, and testability via layer substitution.
|
|
58
|
+
|
|
59
|
+
Exception: an explicit low-level platform implementation that cannot use Effect HTTP. Keep it isolated, lift it with `Effect.tryPromise`, propagate the supplied `AbortSignal`, classify status before decoding, and decode unknown bodies with `Schema`.
|
|
60
|
+
|
|
61
|
+
References: EF-9b, Checklist #42 in effect-first-development.md
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-node-imports
|
|
6
|
+
description: Use Effect platform abstractions instead of node: imports — catch-all for modules without a dedicated rule
|
|
7
|
+
glob: '**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
rule:
|
|
10
|
+
any:
|
|
11
|
+
- all:
|
|
12
|
+
- kind: import_statement
|
|
13
|
+
- regex: '["'']node:[^"'']+["'']'
|
|
14
|
+
- not:
|
|
15
|
+
regex: '["'']node:(?:fs(?:/promises)?|path|os|child_process|https?)["'']'
|
|
16
|
+
- all:
|
|
17
|
+
- pattern: require($SPEC)
|
|
18
|
+
- not:
|
|
19
|
+
regex: 'node:(?:fs(?:/promises)?|path|os|child_process|https?)'
|
|
20
|
+
- all:
|
|
21
|
+
- pattern: import($SPEC)
|
|
22
|
+
- not:
|
|
23
|
+
regex: 'node:(?:fs(?:/promises)?|path|os|child_process|https?)'
|
|
24
|
+
constraints:
|
|
25
|
+
SPEC:
|
|
26
|
+
regex: '^["'']node:[^"'']+["'']$'
|
|
27
|
+
level: warning
|
|
28
|
+
suggestSkills:
|
|
29
|
+
- effect-platform-abstraction
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Use Effect Platform Services Instead of `node:` Imports
|
|
33
|
+
|
|
34
|
+
This is the **catch-all** rule for `node:*` imports that don't have a more specific pattern. Modules with a dedicated rule are excluded here to avoid duplicate diagnostics:
|
|
35
|
+
|
|
36
|
+
| `node:` module | Dedicated rule |
|
|
37
|
+
| ----------------------- | ------------------------------- |
|
|
38
|
+
| `fs`, `fs/promises` | `use-filesystem-service` |
|
|
39
|
+
| `path` | `use-path-service` |
|
|
40
|
+
| `os` | `use-temp-file-scoped` |
|
|
41
|
+
| `child_process` | `use-command-executor-service` |
|
|
42
|
+
| `http`, `https` | `use-http-client-service` |
|
|
43
|
+
|
|
44
|
+
Everything else (`node:stream`, `node:url`, `node:readline`, `node:crypto`, `node:net`, …) falls under this rule.
|
|
45
|
+
|
|
46
|
+
```haskell
|
|
47
|
+
-- Transformation
|
|
48
|
+
import "node:*" :: Node -> IO a -- platform-coupled, untestable
|
|
49
|
+
|
|
50
|
+
-- Instead
|
|
51
|
+
effect services :: Effect a R -- platform-agnostic, testable
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```haskell
|
|
55
|
+
-- Pattern (catch-all examples — covered modules are routed to their dedicated rule)
|
|
56
|
+
bad :: Node -> IO
|
|
57
|
+
bad = do
|
|
58
|
+
stream <- "node:stream" -- → use Stream from effect
|
|
59
|
+
url <- "node:url" -- → URL is a Schema codec
|
|
60
|
+
rl <- "node:readline" -- → use Terminal service
|
|
61
|
+
|
|
62
|
+
good :: Effect a (Stream | Terminal | …)
|
|
63
|
+
good = do
|
|
64
|
+
stream <- Stream.fromReadableStream -- platform-agnostic
|
|
65
|
+
term <- Terminal.Terminal -- from effect
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Platform module mappings (full):**
|
|
69
|
+
|
|
70
|
+
| `node:` import | Effect Platform |
|
|
71
|
+
| -------------------- | --------------------------------------------------------------------- |
|
|
72
|
+
| `node:fs` | `FileSystem.FileSystem` |
|
|
73
|
+
| `node:fs/promises` | `FileSystem.FileSystem` |
|
|
74
|
+
| `node:path` | `Path.Path` |
|
|
75
|
+
| `node:os` | `FileSystem.makeTempFileScoped` / `Path.Path` / platform layer |
|
|
76
|
+
| `node:child_process` | `ChildProcessSpawner` + `ChildProcess` from `effect/unstable/process` |
|
|
77
|
+
| `node:http` | `HttpClient.HttpClient` |
|
|
78
|
+
| `node:https` | `HttpClient.HttpClient` |
|
|
79
|
+
| `node:stream` | `Stream` from effect |
|
|
80
|
+
| `node:readline` | `Terminal.Terminal` |
|
|
81
|
+
| `node:crypto` | `Crypto` from `effect/unstable/crypto` or a wrapped Effect service |
|
|
82
|
+
|
|
83
|
+
**Exceptions:**
|
|
84
|
+
|
|
85
|
+
- Build scripts and tooling config (`vite.config.ts`, `build.ts`, etc.)
|
|
86
|
+
- Platform-specific layers that implement Effect platform services
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-non-null-assertion
|
|
6
|
+
description: Avoid using ! non-null assertion operator
|
|
7
|
+
glob: '**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
pattern: $A!
|
|
10
|
+
level: warning
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Avoid Non-Null Assertion Operator `!`
|
|
14
|
+
|
|
15
|
+
```haskell
|
|
16
|
+
-- Transformation
|
|
17
|
+
bang :: a | Null → a -- "trust me" → runtime crash on null
|
|
18
|
+
safe :: a | Null → Maybe a -- explicit handling required
|
|
19
|
+
|
|
20
|
+
-- Instead
|
|
21
|
+
optional :: a?.b -- optional chaining
|
|
22
|
+
coalesce :: a ?? default -- nullish coalescing
|
|
23
|
+
option :: Option a -- Effect's optional type
|
|
24
|
+
guard :: a → Maybe a -- type guard proves existence
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```haskell
|
|
28
|
+
-- Pattern
|
|
29
|
+
bad :: Map → Value
|
|
30
|
+
bad map = map.get("key")! -- crash if key missing
|
|
31
|
+
|
|
32
|
+
good :: Map → Maybe Value
|
|
33
|
+
good map = Option.fromNullishOr (map.get "key")
|
|
34
|
+
|
|
35
|
+
-- Or with chaining
|
|
36
|
+
safe :: User → Maybe Email
|
|
37
|
+
safe user = user?.contact?.email ?? Nothing
|
|
38
|
+
|
|
39
|
+
-- With Schema for external data
|
|
40
|
+
validated :: Unknown → Either ParseError User
|
|
41
|
+
validated = Schema.decode userSchema
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The `!` operator is "trust me, this isn't null"—if wrong, runtime crash. Use `?.`, `??`, `Option`, or type guards for safe null handling.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-object-type
|
|
6
|
+
description: Avoid using Object or {} as types
|
|
7
|
+
glob: '**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
rule:
|
|
10
|
+
any:
|
|
11
|
+
- all:
|
|
12
|
+
- kind: type_identifier
|
|
13
|
+
- regex: '^Object$'
|
|
14
|
+
- all:
|
|
15
|
+
- kind: object_type
|
|
16
|
+
- regex: '^\{\}$'
|
|
17
|
+
level: warning
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Avoid `Object` and `{}` as Types
|
|
21
|
+
|
|
22
|
+
```haskell
|
|
23
|
+
-- Transformation
|
|
24
|
+
object :: Object -- accepts nearly anything
|
|
25
|
+
empty :: {} -- same problem, no structure
|
|
26
|
+
|
|
27
|
+
-- Instead
|
|
28
|
+
data User = User { id :: Int, name :: String } -- explicit shape
|
|
29
|
+
record :: Record String a -- dictionary with known value type
|
|
30
|
+
unknown :: Unknown -- truly unknown, requires validation
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```haskell
|
|
34
|
+
-- Pattern
|
|
35
|
+
bad :: Object → Effect ()
|
|
36
|
+
bad obj = doSomething obj -- what fields? what types?
|
|
37
|
+
|
|
38
|
+
good :: User → Effect ()
|
|
39
|
+
good user = doSomething user -- clear structure, IDE support
|
|
40
|
+
|
|
41
|
+
-- For unknown shapes
|
|
42
|
+
decode :: Unknown → Either ParseError User
|
|
43
|
+
decode = Schema.decode userSchema
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`Object` and `{}` provide no type safety—they accept any non-null value. Use explicit types, `Record<K,V>`, `unknown`, or Schema for validation.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-option-getorthrow
|
|
6
|
+
description: Avoid Option.getOrThrow - use Option.match or Option.getOrElse for safe unwrapping
|
|
7
|
+
glob: '**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
pattern: $A.getOrThrow
|
|
10
|
+
level: warning
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Avoid `Option.getOrThrow`
|
|
14
|
+
|
|
15
|
+
```haskell
|
|
16
|
+
-- Transformation
|
|
17
|
+
getOrThrow :: Option a -> a -- partial function, may throw
|
|
18
|
+
|
|
19
|
+
-- Instead
|
|
20
|
+
match :: Option a -> { onNone, onSome } -> b -- total, exhaustive
|
|
21
|
+
getOrElse :: (() -> a) -> Option a -> a -- total, with default
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```haskell
|
|
25
|
+
-- Pattern
|
|
26
|
+
bad :: Option User -> User
|
|
27
|
+
bad opt = Option.getOrThrow opt -- throws if None
|
|
28
|
+
|
|
29
|
+
good :: Option User -> String
|
|
30
|
+
good opt = Option.match opt
|
|
31
|
+
{ onNone: \_ -> "unknown"
|
|
32
|
+
, onSome: \u -> u.name
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
also_good :: Option User -> User
|
|
36
|
+
also_good opt = Option.getOrElse opt \_ -> defaultUser
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`Option.getOrThrow` defeats the purpose of using Option. Always handle both cases explicitly.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
action: context
|
|
3
|
+
tool: (edit|write)
|
|
4
|
+
event: after
|
|
5
|
+
name: avoid-platform-coupling
|
|
6
|
+
description: Binding packages should not import platform-specific packages like @effect/platform-bun
|
|
7
|
+
glob: 'packages/*/binding/**/*.{ts,tsx}'
|
|
8
|
+
detector: ast
|
|
9
|
+
rule:
|
|
10
|
+
any:
|
|
11
|
+
- all:
|
|
12
|
+
- kind: import_statement
|
|
13
|
+
- regex: '["'']@effect/platform-(?:bun|node)(?:/[^"'']*)?["'']'
|
|
14
|
+
- pattern: require($SPEC)
|
|
15
|
+
- pattern: import($SPEC)
|
|
16
|
+
constraints:
|
|
17
|
+
SPEC:
|
|
18
|
+
regex: '^["'']@effect/platform-(?:bun|node)(?:/[^"'']*)?["'']$'
|
|
19
|
+
level: warning
|
|
20
|
+
suggestSkills:
|
|
21
|
+
- effect-platform-layers
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Avoid Platform Coupling in Bindings
|
|
25
|
+
|
|
26
|
+
```haskell
|
|
27
|
+
-- Transformation
|
|
28
|
+
concrete :: @effect/platform-bun -- tied to Bun runtime
|
|
29
|
+
abstract :: effect services -- portable across runtimes
|
|
30
|
+
|
|
31
|
+
-- Pattern
|
|
32
|
+
bad :: packages/*/binding/
|
|
33
|
+
bad = import { BunServices } from "@effect/platform-bun"
|
|
34
|
+
bad = Layer.provide(BunServices.layer) -- hardwired platform
|
|
35
|
+
|
|
36
|
+
good :: packages/*/binding/
|
|
37
|
+
good = Layer.provide(platformLayer) -- no platform coupling
|
|
38
|
+
good = -- runtime provides ChildProcessSpawner, FileSystem, etc.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Binding packages wrap external systems (CLIs, APIs, databases) and must be platform-agnostic. They should depend on abstract services from `effect` and its unstable namespaces, such as `FileSystem`, `Path`, `HttpClient`, and `ChildProcessSpawner`, but never on `@effect/platform-bun` or `@effect/platform-node` concrete implementations.
|
|
42
|
+
|
|
43
|
+
Platform-specific layers (`BunServices.layer`, `NodeServices.layer`) belong in the runtime or CLI entry point, not in bindings. The runtime provides concrete implementations of abstract Effect services there.
|