opencode-effect-enforcer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,58 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: effect-run-in-body
6
+ description: Effect.runSync/runPromise/runFork should only be at entry points
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern:
10
+ - Effect.runSync
11
+ - Effect.runPromise
12
+ - Effect.runFork
13
+ level: warning
14
+ suggestSkills:
15
+ - effect-managed-runtime
16
+ ---
17
+
18
+ # Effect.runSync / runPromise / runFork Only at Entry Points
19
+
20
+ ```haskell
21
+ -- Transformation
22
+ runSync :: Effect a E R → a -- escapes Effect, loses composition
23
+ runPromise :: Effect a E R → Promise a -- same problem
24
+ runFork :: Effect a E R → Fiber a -- same problem, async variant
25
+
26
+ -- Instead: compose until boundary
27
+ compose :: Effect a E R → Effect b E R → Effect (a, b) E R
28
+ yield* :: Effect a E R → a -- inside Effect.gen only
29
+ ```
30
+
31
+ ```haskell
32
+ -- Pattern
33
+ bad :: Effect () R
34
+ bad = do
35
+ result ← pure $ Effect.runSync someEffect -- breaks composition!
36
+ doSomething result
37
+ -- can't retry, race, timeout the inner effect
38
+
39
+ good :: Effect () R
40
+ good = do
41
+ result ← someEffect -- still composable
42
+ doSomething result
43
+ -- can wrap entire program with retry, race, timeout
44
+
45
+ -- Entry points only
46
+ main :: IO ()
47
+ main = Effect.runMain program -- ✓ application boundary
48
+
49
+ handler :: Request → IO Response
50
+ handler req = Effect.runPromise (handle req) -- ✓ API boundary
51
+
52
+ test :: Spec
53
+ test = Effect.runPromise (testProgram) -- ✓ test boundary
54
+ ```
55
+
56
+ Running effects mid-logic breaks composition. Keep effects as values until entry points (main, handlers, tests).
57
+
58
+ `Effect.runFork` is just as much a runtime escape as `runSync` / `runPromise`: it materialises a `Fiber` at the call site instead of composing with `Effect.forkChild` / `Effect.forkDetach` (which stay inside the Effect world and respect structural concurrency). Reserve all three for the entrypoint / test harness boundary.
@@ -0,0 +1,76 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: imperative-loops
6
+ description: Use functional transformations instead of imperative loops (for / while / do-while)
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - kind: for_statement
12
+ - kind: for_in_statement
13
+ - kind: while_statement
14
+ - kind: do_statement
15
+ level: warning
16
+ suggestSkills:
17
+ - effect-parallelization
18
+ - effect-scheduling
19
+ ---
20
+
21
+ # Use Functional Transformations
22
+
23
+ ```haskell
24
+ -- Array operations
25
+ map :: [a] → (a → b) → [b]
26
+ filter :: [a] → (a → Bool) → [a]
27
+ reduce :: [a] → b → (b → a → b) → b
28
+ filterMap :: [a] → (a → Option b) → [b] -- single pass
29
+
30
+ -- Record operations
31
+ Record.map :: {k: a} → (a → b) → {k: b}
32
+ Record.filter :: {k: a} → (a → Bool) → {k: a}
33
+ Record.filterMap :: {k: a} → (a → Option b) → {k: b}
34
+
35
+ -- Effectful iteration
36
+ Effect.forEach :: [a] → (a → Effect b) → Effect [b]
37
+ ```
38
+
39
+ ```haskell
40
+ -- Bad: Imperative with mutations (any flavor of loop)
41
+ bad₁ :: [Number] → [Number]
42
+ bad₁ numbers = do
43
+ result ← []
44
+ for n in numbers do -- ✗ ForOf
45
+ if n % 2 == 0 then result.push(n * n)
46
+ return result
47
+
48
+ bad₂ :: Effect ()
49
+ bad₂ = do
50
+ while (not done) do -- ✗ While
51
+ yield* step
52
+
53
+ bad₃ :: Effect ()
54
+ bad₃ = do
55
+ repeat -- ✗ DoWhile
56
+ yield* step
57
+ until done
58
+
59
+ -- Good: Functional composition
60
+ good :: [Number] → [Number]
61
+ good numbers =
62
+ filterMap numbers λn →
63
+ if n % 2 == 0
64
+ then Option.some(n * n)
65
+ else Option.none()
66
+
67
+ goodEffect :: [Item] → Effect ()
68
+ goodEffect items = Effect.forEach items processItem { concurrency: 4 }
69
+
70
+ goodRecurring :: Effect ()
71
+ goodRecurring = step & Effect.repeat (Schedule.spaced (Duration.seconds 1))
72
+ ```
73
+
74
+ Imperative loops — `for`, `for ... in`, `for ... of`, `while`, `do ... while` — encourage mutation and break composition. Use `Arr.map` / `Arr.filter` / `Arr.filterMap` / `Arr.reduce` for pure transformations, `Effect.forEach` for effectful iteration (with explicit `concurrency`), and `Effect.repeat` plus `Schedule` for recurring effects.
75
+
76
+ Note: in tree-sitter TypeScript, `for ... of` and `for ... in` share the same AST kind (`for_in_statement`), so listing `for_in_statement` covers both.
@@ -0,0 +1,52 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: prefer-arr-sort
6
+ description: Use Arr.sort with explicit Order instead of native Array.prototype.sort
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ pattern: $A.sort($$$)
11
+ not:
12
+ pattern: Arr.sort($$$)
13
+ level: warning
14
+ ---
15
+
16
+ # Use `Arr.sort` with Explicit `Order`
17
+
18
+ ```haskell
19
+ -- Transformation
20
+ native :: [a] -> (a -> a -> Number) -> [a] -- mutates in place, untyped comparator
21
+ arrSort :: [a] -> Order a -> [a] -- pure, typed, composable
22
+
23
+ -- Pattern
24
+ bad :: [User] -> [User]
25
+ bad users = users.sort((a, b) => a.name.localeCompare(b.name))
26
+ -- mutates original, comparator is untyped
27
+
28
+ good :: [User] -> [User]
29
+ good users = Arr.sort(users, byName)
30
+ where byName = Order.mapInput(Order.String, (u: User) => u.name)
31
+ -- pure copy, typed ordering, composable
32
+ ```
33
+
34
+ ```haskell
35
+ -- Composing orders
36
+ byNameThenAge :: Order User
37
+ byNameThenAge = Order.combine(
38
+ Order.mapInput(Order.String, _.name),
39
+ Order.mapInput(Order.Number, _.age)
40
+ )
41
+
42
+ sorted :: [User] -> [User]
43
+ sorted = Arr.sort byNameThenAge
44
+
45
+ -- Reverse
46
+ descending :: [User] -> [User]
47
+ descending = Arr.sort (Order.reverse byName)
48
+ ```
49
+
50
+ Native `.sort()` mutates the array in place and uses an untyped comparator. `Arr.sort` from `effect/Array` returns a new sorted array using a composable, typed `Order`.
51
+
52
+ References: EF-38 in effect-first-development.md
@@ -0,0 +1,56 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: prefer-duration-values
6
+ description: Use Duration helpers instead of numeric duration literals
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - pattern: 'Effect.sleep($DURATION)'
12
+ - pattern: 'Effect.delay($EFFECT, $DURATION)'
13
+ - pattern: 'Effect.delay($DURATION)'
14
+ - pattern: 'Effect.timeout($EFFECT, $DURATION)'
15
+ - pattern: 'Effect.timeout($DURATION)'
16
+ - pattern: 'Effect.timeoutOption($EFFECT, $DURATION)'
17
+ - pattern: 'Effect.timeoutOption($DURATION)'
18
+ - pattern: 'Effect.timeoutOrElse($EFFECT, { duration: $DURATION, $$$REST })'
19
+ - pattern: 'Effect.timeoutOrElse({ duration: $DURATION, $$$REST })'
20
+ - pattern: 'Schedule.duration($DURATION)'
21
+ - pattern: 'Schedule.during($DURATION)'
22
+ - pattern: 'Schedule.exponential($DURATION)'
23
+ - pattern: 'Schedule.fibonacci($DURATION)'
24
+ - pattern: 'Schedule.fixed($DURATION)'
25
+ - pattern: 'Schedule.spaced($DURATION)'
26
+ - pattern: 'Schedule.windowed($DURATION)'
27
+ constraints:
28
+ DURATION:
29
+ kind: number
30
+ level: warning
31
+ suggestSkills:
32
+ - effect-scheduling
33
+ - effect-testing
34
+ ---
35
+
36
+ # Prefer `Duration` Values
37
+
38
+ ```haskell
39
+ -- Transformation
40
+ number :: milliseconds? seconds? unclear
41
+ Duration :: explicit time unit
42
+ ```
43
+
44
+ ```typescript
45
+ // Bad
46
+ Effect.sleep(1000);
47
+ program.pipe(Effect.timeout(5000));
48
+ Schedule.spaced(250);
49
+
50
+ // Good
51
+ Effect.sleep(Duration.seconds(1));
52
+ program.pipe(Effect.timeout(Duration.seconds(5)));
53
+ Schedule.spaced(Duration.millis(250));
54
+ ```
55
+
56
+ Numeric duration literals obscure units and make timeout/retry policies harder to review. Use `Duration.millis`, `Duration.seconds`, or another `effect/Duration` constructor so time windows are explicit.
@@ -0,0 +1,161 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: prefer-effect-fn
6
+ description: Service methods should use Effect.fn for automatic tracing instead of plain Effect.gen wrappers
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - pattern: ($$$ARGS) => Effect.gen($$$BODY)
12
+ - pattern: '$NAME: ($$$ARGS) => Effect.gen($$$BODY)'
13
+ - all:
14
+ - kind: method_definition
15
+ - has:
16
+ field: body
17
+ regex: Effect\.gen
18
+ - all:
19
+ - kind: pair
20
+ - has:
21
+ pattern: function($$$ARGS) { return Effect.gen($$$BODY) }
22
+ inside:
23
+ any:
24
+ - pattern: Layer.effect($$$)
25
+ - pattern: Layer.effectContext($$$)
26
+ - pattern: Layer.succeed($$$)
27
+ stopBy: end
28
+ not:
29
+ inside:
30
+ pattern: Effect.fn($$$)($$$)
31
+ stopBy: end
32
+ level: warning
33
+ suggestSkills:
34
+ - effect-service-implementation
35
+ ---
36
+
37
+ # Prefer `Effect.fn` for Service Methods
38
+
39
+ ```haskell
40
+ -- Transformation
41
+ Effect.gen :: (() -> Generator) -> Effect -- no tracing, anonymous
42
+ Effect.fn :: String -> (...args -> Effect) -- named, auto-traced span
43
+ ```
44
+
45
+ ```haskell
46
+ -- Pattern
47
+ bad :: Service method
48
+ bad = {
49
+ getUser: (id: UserId) =>
50
+ Effect.gen(function* () { -- anonymous, no span
51
+ ...
52
+ })
53
+ }
54
+
55
+ good :: Service method
56
+ good = {
57
+ getUser: Effect.fn("UserService.getUser")(
58
+ (id: UserId) => -- named span, auto-traced
59
+ Effect.gen(function* () {
60
+ ...
61
+ })
62
+ )
63
+ }
64
+ ```
65
+
66
+ `Effect.fn` wraps a function to automatically create a traced span with the given name. Use it for all service method implementations to get observability for free.
67
+
68
+ ## Format
69
+
70
+ ```typescript
71
+ const methodName = Effect.fn('ServiceName.methodName')(function* (
72
+ arg1: Type1,
73
+ arg2: Type2
74
+ ) {
75
+ // implementation using yield*
76
+ });
77
+ ```
78
+
79
+ Key details:
80
+
81
+ - **Naming convention**: `"ServiceName.methodName"` — matches the service class and method
82
+ - **Generator shorthand**: Pass a generator function directly to `Effect.fn` — no need for an intermediate arrow wrapping `Effect.gen`
83
+ - **Wraps the function**: `Effect.fn` takes the entire implementation function as a generator
84
+
85
+ ## Complete Before/After
86
+
87
+ ```typescript
88
+ // BEFORE — plain arrow functions, no tracing
89
+ export class UserRepository extends Context.Service<UserRepository>()(
90
+ '@services/UserRepository',
91
+ {
92
+ make: Effect.gen(function* () {
93
+ const db = yield* DatabaseClient;
94
+
95
+ return {
96
+ findById: (id: string): Effect.Effect<User, UserNotFound> =>
97
+ Effect.gen(function* () {
98
+ const row = yield* db.query(
99
+ 'SELECT * FROM users WHERE id = ?',
100
+ id
101
+ );
102
+ if (!row)
103
+ return yield* new UserNotFound({
104
+ id,
105
+ message: `Not found: ${id}`
106
+ });
107
+ return row as User;
108
+ }),
109
+
110
+ create: (
111
+ data: CreateUserData
112
+ ): Effect.Effect<User, DuplicateUser> =>
113
+ Effect.gen(function* () {
114
+ return yield* db.insert('users', data);
115
+ })
116
+ };
117
+ })
118
+ }
119
+ ) {}
120
+
121
+ // AFTER — Effect.fn, every method gets a traced span
122
+ export class UserRepository extends Context.Service<UserRepository>()(
123
+ '@services/UserRepository',
124
+ {
125
+ make: Effect.gen(function* () {
126
+ const db = yield* DatabaseClient;
127
+
128
+ const findById = Effect.fn('UserRepository.findById')(
129
+ (id: string): Effect.Effect<User, UserNotFound> =>
130
+ Effect.gen(function* () {
131
+ const row = yield* db.query(
132
+ 'SELECT * FROM users WHERE id = ?',
133
+ id
134
+ );
135
+ if (!row)
136
+ return yield* new UserNotFound({
137
+ id,
138
+ message: `Not found: ${id}`
139
+ });
140
+ return row as User;
141
+ })
142
+ );
143
+
144
+ const create = Effect.fn('UserRepository.create')(
145
+ (data: CreateUserData): Effect.Effect<User, DuplicateUser> =>
146
+ Effect.gen(function* () {
147
+ return yield* db.insert('users', data);
148
+ })
149
+ );
150
+
151
+ return { findById, create };
152
+ })
153
+ }
154
+ ) {}
155
+ ```
156
+
157
+ ## When NOT to use Effect.fn
158
+
159
+ - Top-level programs or scripts (not service methods)
160
+ - One-off effects that aren't part of a service interface
161
+ - Simple succeed/fail expressions that don't benefit from tracing
@@ -0,0 +1,48 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: prefer-match-over-switch
6
+ description: Use Match from Effect instead of native switch statements
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern: 'switch ($X) { $$$ }'
10
+ level: warning
11
+ suggestSkills:
12
+ - effect-pattern-matching
13
+ ---
14
+
15
+ # Use `Match` Instead of `switch`
16
+
17
+ ```haskell
18
+ -- Transformation
19
+ switch :: a -> { case₁: b, ..., default: b } -- non-exhaustive, fall-through risk
20
+ Match :: a -> Matcher a b -- exhaustive, composable, type-safe
21
+
22
+ -- Pattern
23
+ bad :: Phase -> String
24
+ bad phase = switch phase of
25
+ "idle" -> "waiting"
26
+ "running" -> "active"
27
+ _ -> "unknown" -- default hides new variants
28
+
29
+ good :: Phase -> String
30
+ good phase = Match.value(phase).pipe(
31
+ Match.when("idle", \_ -> "waiting"),
32
+ Match.when("running", \_ -> "active"),
33
+ Match.exhaustive -- compiler error if variant added
34
+ )
35
+ ```
36
+
37
+ ```haskell
38
+ -- For tagged unions
39
+ matchTagged :: Event -> String
40
+ matchTagged = Match.value >>> pipe
41
+ Match.tag("Created", \e -> "new: " <> e.id)
42
+ Match.tag("Completed", \e -> "done: " <> e.id)
43
+ Match.exhaustive
44
+ ```
45
+
46
+ `switch` has fall-through semantics and non-exhaustive `default` cases that hide new variants at compile time. `Match` from Effect is exhaustive, composable, and type-safe — the compiler will error when new variants are added.
47
+
48
+ References: EF-7 in effect-first-development.md
@@ -0,0 +1,56 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: prefer-option-over-null
6
+ description: Consider using Option instead of union with null or undefined
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - all:
12
+ - kind: literal_type
13
+ - regex: '^null$'
14
+ - inside:
15
+ kind: union_type
16
+ stopBy: neighbor
17
+ - all:
18
+ - kind: literal_type
19
+ - regex: '^undefined$'
20
+ - inside:
21
+ kind: union_type
22
+ stopBy: neighbor
23
+ level: info
24
+ ---
25
+
26
+ # Consider `Option` Instead of `| null` / `| undefined`
27
+
28
+ ```haskell
29
+ -- Transformation
30
+ nullable :: T | Null -- scattered null checks
31
+ undef :: T | Undefined -- same problem, different keyword
32
+ option :: Option T -- composable, chainable
33
+
34
+ -- Option operations
35
+ map :: (a → b) → Option a → Option b
36
+ flatMap :: (a → Option b) → Option a → Option b
37
+ filter :: (a → Bool) → Option a → Option a
38
+ getOrElse :: a → Option a → a
39
+ ```
40
+
41
+ ```haskell
42
+ -- Pattern
43
+ bad :: Id → User | Null
44
+ bad id = users.get id -- caller must check null
45
+
46
+ good :: Id → Option User
47
+ good id = Option.fromNullishOr (users.get id)
48
+
49
+ -- Composition
50
+ findEmail :: Id → Option Email
51
+ findEmail = good >=> (_.email >>> Option.fromNullishOr)
52
+ ```
53
+
54
+ `Option<T>` provides chainable operations. Use `| null` or `| undefined` only at external boundaries (JSON, DOM, third-party libs).
55
+
56
+ Both `| null` and `| undefined` carry the same modelling cost — every consumer needs a defensive check, and `Option`'s combinators (`map`, `flatMap`, `filter`, `getOrElse`, `match`) replace those checks with a single composable shape. `| null | undefined` is the worst of both; convert it at the boundary with `Option.fromNullishOr`.
@@ -0,0 +1,70 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: prefer-redacted-config
6
+ description: Use Config.redacted or Schema.Redacted for secret-like configuration values
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ any:
11
+ - pattern: Config.string($KEY)
12
+ - pattern: Config.nonEmptyString($KEY)
13
+ - all:
14
+ - kind: pair
15
+ - has:
16
+ field: key
17
+ regex: '(?i)(api[_-]?key|auth[_-]?token|token|secret|password|passwd|private[_-]?key|client[_-]?secret|database[_-]?url|db[_-]?url|connection[_-]?string|dsn)'
18
+ - has:
19
+ field: value
20
+ regex: '^Schema\.(String|NonEmptyString)(\.|$)'
21
+ - inside:
22
+ pattern: Config.schema($$$)
23
+ stopBy: end
24
+ constraints:
25
+ KEY:
26
+ regex: '(?i)^["''][^"'']*(api[_-]?key|auth[_-]?token|token|secret|password|passwd|private[_-]?key|client[_-]?secret|database[_-]?url|db[_-]?url|connection[_-]?string|dsn)[^"'']*["'']$'
27
+ level: warning
28
+ suggestSkills:
29
+ - effect-config
30
+ ---
31
+
32
+ # Prefer Redacted Config for Secrets
33
+
34
+ ```haskell
35
+ -- Transformation
36
+ Config.string secretKey :: String -- easy to log accidentally
37
+ Config.redacted secretKey :: Redacted String -- hidden from logs/toString
38
+ ```
39
+
40
+ ```typescript
41
+ // Bad
42
+ const apiKey = Config.string('API_KEY');
43
+ const token = Config.nonEmptyString('GITHUB_TOKEN');
44
+
45
+ // Good
46
+ const apiKey = Config.redacted('API_KEY');
47
+ const token = Config.redacted('GITHUB_TOKEN');
48
+ ```
49
+
50
+ For structured config schemas, wrap secret-like string fields in `Schema.Redacted`:
51
+
52
+ ```typescript
53
+ // Bad
54
+ const AppConfig = Config.schema(
55
+ Schema.Struct({
56
+ apiKey: Schema.String,
57
+ password: Schema.NonEmptyString
58
+ })
59
+ );
60
+
61
+ // Good
62
+ const AppConfig = Config.schema(
63
+ Schema.Struct({
64
+ apiKey: Schema.Redacted(Schema.String),
65
+ password: Schema.Redacted(Schema.String)
66
+ })
67
+ );
68
+ ```
69
+
70
+ Secrets should remain redacted from the moment they enter the program. Use `Config.redacted` for primitive config values and `Schema.Redacted(Schema.String)` for schema-based config fields.
@@ -0,0 +1,54 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: prefer-schema-class
6
+ description: Review Schema.Struct used for decoded or domain objects; prefer Schema.Class when identity matters
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ pattern: Schema.Struct($$$)
10
+ level: info
11
+ suggestSkills:
12
+ - effect-domain-modeling
13
+ ---
14
+
15
+ # Use `Schema.Class` Instead of `Schema.Struct`
16
+
17
+ ```haskell
18
+ -- Transformation
19
+ Schema.Struct :: { fields } -> Schema { fields } -- anonymous, no constructor
20
+ Schema.Class :: String -> { fields } -> Class -- named, constructable, extensible
21
+
22
+ -- Pattern
23
+ bad :: Schema
24
+ bad = Schema.Struct({
25
+ id: Schema.String,
26
+ name: Schema.String
27
+ })
28
+ -- anonymous type, no constructor, no instanceof
29
+
30
+ good :: Schema
31
+ good = class User extends Schema.Class<User>("User")({
32
+ id: Schema.String,
33
+ name: Schema.String
34
+ }) {}
35
+ -- named type, constructor, instanceof, optional annotations when useful
36
+ ```
37
+
38
+ ```haskell
39
+ -- Benefits of Schema.Class
40
+ construct :: User
41
+ construct = new User({ id: "1", name: "Alice" })
42
+
43
+ check :: unknown -> Bool
44
+ check = (x) => x instanceof User
45
+
46
+ extend :: Schema
47
+ extend = class Admin extends User.extend<Admin>("Admin")({
48
+ role: Schema.Literal("admin")
49
+ }) {}
50
+ ```
51
+
52
+ `Schema.Struct` produces an anonymous schema without a constructor or `instanceof` support. `Schema.Class` provides a named type, constructor, extensibility, and optional annotation support when docs or introspection benefit from it. Prefer `Schema.Class` for decoded domain/API shapes, union members, and values that need identity. `Schema.Struct` remains appropriate for local structural composition, configuration internals, and schemas where class identity adds no value, so review this informational finding in context.
53
+
54
+ References: EF-3, EF-33 in effect-first-development.md