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,274 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-context-witness
|
|
3
|
+
description: Decide between Context.Service witness and capability patterns for dependency injection, understanding coupling trade-offs
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Context Witness Pattern
|
|
7
|
+
|
|
8
|
+
Choose between witness (existence) and capability (behavior) patterns for Context.Service definitions.
|
|
9
|
+
|
|
10
|
+
## Coupling: Hard vs Soft
|
|
11
|
+
|
|
12
|
+
**Some coupling is necessary and good** - but move it from hard to soft coupling.
|
|
13
|
+
|
|
14
|
+
### Hard Coupling (Schema)
|
|
15
|
+
|
|
16
|
+
Field exists in the schema - tightly coupled to domain model:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { Schema } from 'effect';
|
|
20
|
+
|
|
21
|
+
// ❌ HARD COUPLING - Serial is part of the schema
|
|
22
|
+
export const PaymentIntent = Schema.Struct({
|
|
23
|
+
id: Schema.String,
|
|
24
|
+
serial: Schema.String, // In schema = hard coupled
|
|
25
|
+
amount: Schema.BigInt
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Every PaymentIntent MUST have a serial
|
|
29
|
+
// Serialization/validation requires serial
|
|
30
|
+
// Cannot create without providing serial
|
|
31
|
+
// Schema change needed to remove/change serial
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Soft Coupling (Witness)
|
|
35
|
+
|
|
36
|
+
Field **removed from schema**, only injected in code:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Schema, Context, Effect, Logger } from 'effect';
|
|
40
|
+
|
|
41
|
+
declare const generateId: () => string;
|
|
42
|
+
|
|
43
|
+
// ✅ SOFT COUPLING - Serial not in schema
|
|
44
|
+
export const PaymentIntent = Schema.Struct({
|
|
45
|
+
id: Schema.String,
|
|
46
|
+
amount: Schema.BigInt
|
|
47
|
+
// No serial field!
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Serial is a witness - required but injected via Context
|
|
51
|
+
class Serial extends Context.Service<Serial, string>()('Serial') {}
|
|
52
|
+
|
|
53
|
+
const createPaymentIntent = (amount: bigint) =>
|
|
54
|
+
Effect.gen(function* () {
|
|
55
|
+
const serial = yield* Serial; // Injected from context
|
|
56
|
+
|
|
57
|
+
// Use serial in business logic, logging, etc.
|
|
58
|
+
// but it's not part of the persisted data
|
|
59
|
+
yield* Logger.info(`Creating payment intent ${serial}`);
|
|
60
|
+
|
|
61
|
+
return PaymentIntent.make({ id: generateId(), amount });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Type: Effect<PaymentIntent, never, Serial>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Key insight:** `schema (hard coupling) => witness (soft coupling)`
|
|
68
|
+
|
|
69
|
+
By removing the field from the schema and injecting it only where needed, you:
|
|
70
|
+
|
|
71
|
+
- Keep domain models minimal
|
|
72
|
+
- Avoid unnecessary persistence
|
|
73
|
+
- Easy to test (provide test serial)
|
|
74
|
+
- Easy to remove/change (just change injection)
|
|
75
|
+
- Explicit dependencies in type signature
|
|
76
|
+
|
|
77
|
+
**When to use witnesses:**
|
|
78
|
+
|
|
79
|
+
- Correlation IDs (for tracing, not persistence)
|
|
80
|
+
- Request IDs (for logging, not data)
|
|
81
|
+
- Transaction contexts (for coordination, not storage)
|
|
82
|
+
- Tenant/Region markers (for routing, not schema)
|
|
83
|
+
|
|
84
|
+
## Witness: Existence Only
|
|
85
|
+
|
|
86
|
+
Use when you only need to know something **exists** in the environment:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { Schema, Context, Effect } from 'effect';
|
|
90
|
+
|
|
91
|
+
declare const PaymentIntent: Schema.Struct<{
|
|
92
|
+
id: typeof Schema.String;
|
|
93
|
+
serial: typeof Schema.String;
|
|
94
|
+
amount: typeof Schema.BigInt;
|
|
95
|
+
}>;
|
|
96
|
+
declare const other: any;
|
|
97
|
+
|
|
98
|
+
// Witness - a serial number exists
|
|
99
|
+
export class Serial extends Context.Service<Serial, string>()('Serial') {}
|
|
100
|
+
|
|
101
|
+
const createPaymentIntent = Effect.gen(function* () {
|
|
102
|
+
const serial = yield* Serial; // Pull from environment
|
|
103
|
+
return PaymentIntent.make({ serial, ...other });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Type: Effect<PaymentIntent, never, Serial>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Capability: Behavior
|
|
110
|
+
|
|
111
|
+
Use when you need **operations**:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { Schema, Context, Effect } from 'effect';
|
|
115
|
+
|
|
116
|
+
declare const PaymentIntent: Schema.Struct<{
|
|
117
|
+
id: typeof Schema.String;
|
|
118
|
+
serial: typeof Schema.String;
|
|
119
|
+
amount: typeof Schema.BigInt;
|
|
120
|
+
}>;
|
|
121
|
+
declare const other: any;
|
|
122
|
+
|
|
123
|
+
// Capability - can generate/validate
|
|
124
|
+
export class SerialService extends Context.Service<
|
|
125
|
+
SerialService,
|
|
126
|
+
{
|
|
127
|
+
readonly next: () => string;
|
|
128
|
+
readonly validate: (s: string) => boolean;
|
|
129
|
+
}
|
|
130
|
+
>()('SerialService') {}
|
|
131
|
+
|
|
132
|
+
const createPaymentIntent = Effect.gen(function* () {
|
|
133
|
+
const svc = yield* SerialService;
|
|
134
|
+
const serial = svc.next(); // Behavior
|
|
135
|
+
return PaymentIntent.make({ serial, ...other });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Type: Effect<PaymentIntent, never, SerialService>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Decision Framework
|
|
142
|
+
|
|
143
|
+
| Need | Pattern |
|
|
144
|
+
| ------------------------ | ---------- |
|
|
145
|
+
| Just presence/value | Witness |
|
|
146
|
+
| Operations/generation | Capability |
|
|
147
|
+
| Precondition marker | Witness |
|
|
148
|
+
| Side effects | Capability |
|
|
149
|
+
| Multiple implementations | Capability |
|
|
150
|
+
| Mocking behavior | Capability |
|
|
151
|
+
| Correlation ID | Witness |
|
|
152
|
+
| Transaction context | Witness |
|
|
153
|
+
| Logger | Capability |
|
|
154
|
+
| Database | Capability |
|
|
155
|
+
|
|
156
|
+
## When to Use Witness
|
|
157
|
+
|
|
158
|
+
Good fits:
|
|
159
|
+
|
|
160
|
+
- **Request ID** - must exist for tracing
|
|
161
|
+
- **Transaction context** - must be established
|
|
162
|
+
- **Tenant/Region** - required for data boundary
|
|
163
|
+
- **Pre-validated tokens** - already verified
|
|
164
|
+
|
|
165
|
+
## When to Use Capability
|
|
166
|
+
|
|
167
|
+
Good fits:
|
|
168
|
+
|
|
169
|
+
- **Serial generation** - create/validate operations
|
|
170
|
+
- **Clock** - `now()` operation
|
|
171
|
+
- **Logger** - structured logging methods
|
|
172
|
+
- **Database** - query/transact operations
|
|
173
|
+
- **HTTP clients** - fetch/post operations
|
|
174
|
+
|
|
175
|
+
## Testing Implications
|
|
176
|
+
|
|
177
|
+
Witnesses are trivial to provide:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { Effect } from 'effect';
|
|
181
|
+
|
|
182
|
+
declare const myProgram: Effect.Effect<unknown, never, Serial>;
|
|
183
|
+
declare class Serial extends Context.Service<Serial, string>()('Serial') {}
|
|
184
|
+
|
|
185
|
+
const test = myProgram.pipe(Effect.provideService(Serial, 'test-serial-123'));
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Capabilities need implementation:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { Effect } from 'effect';
|
|
192
|
+
|
|
193
|
+
declare const myProgram: Effect.Effect<unknown, never, SerialService>;
|
|
194
|
+
declare class SerialService extends Context.Service<
|
|
195
|
+
SerialService,
|
|
196
|
+
{
|
|
197
|
+
readonly next: () => string;
|
|
198
|
+
readonly validate: (s: string) => boolean;
|
|
199
|
+
}
|
|
200
|
+
>()('SerialService') {}
|
|
201
|
+
|
|
202
|
+
const test = myProgram.pipe(
|
|
203
|
+
Effect.provideService(SerialService, {
|
|
204
|
+
next: () => 'test-serial-123',
|
|
205
|
+
validate: () => true
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Coupling Strategy
|
|
211
|
+
|
|
212
|
+
**Rule of thumb**: Remove non-essential fields from schema, inject via witness instead.
|
|
213
|
+
|
|
214
|
+
**Ask yourself:** Does this need to be persisted/serialized?
|
|
215
|
+
|
|
216
|
+
- **No** → Remove from schema, inject via witness
|
|
217
|
+
- **Yes** → Keep in schema
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { Schema, Context, Effect, Logger, Clock } from 'effect';
|
|
221
|
+
|
|
222
|
+
declare const LineItem: Schema.Schema<any>;
|
|
223
|
+
declare const generateId: () => string;
|
|
224
|
+
declare const calculateTotal: (items: Array<any>) => bigint;
|
|
225
|
+
|
|
226
|
+
// ✅ Domain model - only persisted data
|
|
227
|
+
export const Order = Schema.Struct({
|
|
228
|
+
id: Schema.String,
|
|
229
|
+
items: Schema.Array(LineItem),
|
|
230
|
+
total: Schema.BigInt
|
|
231
|
+
// No correlationId - not persisted!
|
|
232
|
+
// No timestamp - derived from system!
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Witnesses for runtime context
|
|
236
|
+
class CorrelationId extends Context.Service<CorrelationId, string>()(
|
|
237
|
+
'CorrelationId'
|
|
238
|
+
) {}
|
|
239
|
+
class RequestId extends Context.Service<RequestId, string>()('RequestId') {}
|
|
240
|
+
|
|
241
|
+
// Use in code, not in data
|
|
242
|
+
const createOrder = (items: Array<Schema.Schema.Type<typeof LineItem>>) =>
|
|
243
|
+
Effect.gen(function* () {
|
|
244
|
+
const correlationId = yield* CorrelationId; // For tracing
|
|
245
|
+
const requestId = yield* RequestId; // For logging
|
|
246
|
+
const timestamp = yield* Clock.currentTimeMillis; // For timestamp
|
|
247
|
+
|
|
248
|
+
yield* Logger.info({
|
|
249
|
+
message: 'Creating order',
|
|
250
|
+
correlationId, // Used for tracing
|
|
251
|
+
requestId, // Used for logging
|
|
252
|
+
timestamp
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Data only contains what's persisted
|
|
256
|
+
return Order.make({
|
|
257
|
+
id: generateId(),
|
|
258
|
+
items,
|
|
259
|
+
total: calculateTotal(items)
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Type: Effect<Order, never, CorrelationId | RequestId>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Benefits:**
|
|
267
|
+
|
|
268
|
+
- Minimal schemas (only persisted data)
|
|
269
|
+
- Context values available when needed
|
|
270
|
+
- Easy to test with different context
|
|
271
|
+
- Can add/remove context without schema changes
|
|
272
|
+
- Explicit dependencies in type signatures
|
|
273
|
+
|
|
274
|
+
Choose witness for simplicity, capability for flexibility.
|