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