@zeix/cause-effect 1.0.0 → 1.0.2
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/.github/copilot-instructions.md +5 -1
- package/.zed/settings.json +24 -1
- package/ARCHITECTURE.md +27 -1
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +1 -1
- package/GUIDE.md +2 -5
- package/README.md +45 -3
- package/REQUIREMENTS.md +3 -3
- package/eslint.config.js +2 -1
- package/index.dev.js +14 -0
- package/index.js +1 -1
- package/index.ts +1 -1
- package/package.json +5 -4
- package/skills/cause-effect/SKILL.md +69 -0
- package/skills/cause-effect/agents/openai.yaml +4 -0
- package/skills/cause-effect/references/api-facts.md +179 -0
- package/skills/cause-effect/references/error-classes.md +153 -0
- package/skills/cause-effect/references/non-obvious-behaviors.md +195 -0
- package/skills/cause-effect/references/signal-types.md +292 -0
- package/skills/cause-effect/workflows/answer-question.md +54 -0
- package/skills/cause-effect/workflows/debug.md +71 -0
- package/skills/cause-effect/workflows/use-api.md +63 -0
- package/skills/cause-effect-dev/SKILL.md +61 -100
- package/skills/cause-effect-dev/references/api-facts.md +96 -0
- package/skills/cause-effect-dev/references/error-classes.md +97 -0
- package/skills/cause-effect-dev/references/internal-types.md +54 -0
- package/skills/cause-effect-dev/references/non-obvious-behaviors.md +162 -0
- package/skills/cause-effect-dev/references/source-map.md +45 -0
- package/skills/cause-effect-dev/workflows/answer-question.md +55 -0
- package/skills/cause-effect-dev/workflows/fix-bug.md +63 -0
- package/skills/cause-effect-dev/workflows/implement-feature.md +46 -0
- package/skills/cause-effect-dev/workflows/write-tests.md +64 -0
- package/skills/changelog-keeper/SKILL.md +47 -37
- package/skills/tech-writer/SKILL.md +94 -0
- package/skills/tech-writer/references/document-map.md +199 -0
- package/skills/tech-writer/references/tone-guide.md +189 -0
- package/skills/tech-writer/workflows/consistency-review.md +98 -0
- package/skills/tech-writer/workflows/update-after-change.md +65 -0
- package/skills/tech-writer/workflows/update-agent-docs.md +77 -0
- package/skills/tech-writer/workflows/update-architecture.md +61 -0
- package/skills/tech-writer/workflows/update-jsdoc.md +72 -0
- package/skills/tech-writer/workflows/update-public-api.md +59 -0
- package/skills/tech-writer/workflows/update-requirements.md +80 -0
- package/src/graph.ts +2 -0
- package/src/nodes/collection.ts +38 -0
- package/src/nodes/effect.ts +13 -1
- package/src/nodes/list.ts +41 -2
- package/src/nodes/memo.ts +0 -1
- package/src/nodes/sensor.ts +10 -4
- package/src/nodes/store.ts +11 -0
- package/src/signal.ts +6 -0
- package/test/list.test.ts +121 -0
- package/tsconfig.json +9 -0
- package/types/index.d.ts +1 -1
- package/types/src/graph.d.ts +2 -0
- package/types/src/nodes/collection.d.ts +38 -0
- package/types/src/nodes/effect.d.ts +13 -1
- package/types/src/nodes/list.d.ts +30 -2
- package/types/src/nodes/memo.d.ts +0 -1
- package/types/src/nodes/sensor.d.ts +10 -4
- package/types/src/nodes/store.d.ts +11 -0
- package/types/src/signal.d.ts +6 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<overview>
|
|
2
|
+
What each signal type in @zeix/cause-effect is for, when to use each one, and how to choose between similar types. All knowledge is embedded — no external files required.
|
|
3
|
+
</overview>
|
|
4
|
+
|
|
5
|
+
<signal_catalog>
|
|
6
|
+
|
|
7
|
+
<State>
|
|
8
|
+
**What it is:** A mutable reactive value you own and update explicitly.
|
|
9
|
+
|
|
10
|
+
**Use when:**
|
|
11
|
+
- You control when and how the value changes
|
|
12
|
+
- The value is UI state, form input, a counter, a toggle, a selection
|
|
13
|
+
- You need to write to it directly with `.set()`
|
|
14
|
+
|
|
15
|
+
**Key facts:**
|
|
16
|
+
- Requires an initial value
|
|
17
|
+
- Synchronous reads and writes
|
|
18
|
+
- Supports `equals` (default `===`) and `guard` options
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
const count = createState(0)
|
|
22
|
+
count.set(count.get() + 1)
|
|
23
|
+
count.update(n => n + 1) // if update helper exists; else use set
|
|
24
|
+
```
|
|
25
|
+
</State>
|
|
26
|
+
|
|
27
|
+
<Sensor>
|
|
28
|
+
**What it is:** A reactive value produced by an external source you don't control.
|
|
29
|
+
|
|
30
|
+
**Use when:**
|
|
31
|
+
- The value comes from outside the reactive graph: DOM events, timers, WebSocket messages, geolocation, device orientation, IntersectionObserver, etc.
|
|
32
|
+
- You need `watched`/`unwatched` hooks to start and stop the external subscription efficiently
|
|
33
|
+
- The value has no meaningful initial state before the source fires
|
|
34
|
+
|
|
35
|
+
**Key facts:**
|
|
36
|
+
- Starts **unset** — reading before the first value throws `UnsetSignalValueError`; use `match` to handle the initial state
|
|
37
|
+
- `watched` fires when the first downstream effect subscribes; `unwatched` fires when the last one unsubscribes
|
|
38
|
+
- The setup function receives a `set` callback; return a cleanup function to tear down the subscription
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const pointer = createSensor<{ x: number; y: number }>(set => {
|
|
42
|
+
const handler = (e: PointerEvent) => set({ x: e.clientX, y: e.clientY })
|
|
43
|
+
window.addEventListener('pointermove', handler)
|
|
44
|
+
return () => window.removeEventListener('pointermove', handler)
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
</Sensor>
|
|
48
|
+
|
|
49
|
+
<Memo>
|
|
50
|
+
**What it is:** A synchronously derived value that stays in sync with its dependencies.
|
|
51
|
+
|
|
52
|
+
**Use when:**
|
|
53
|
+
- The value can be computed from other signals without async work
|
|
54
|
+
- You want to avoid recomputing an expensive derivation on every read
|
|
55
|
+
- You need a stable reference: Memo caches the last computed value and only recomputes when dependencies change
|
|
56
|
+
|
|
57
|
+
**Key facts:**
|
|
58
|
+
- Lazy — only recomputes when read after a dependency has changed
|
|
59
|
+
- Receives `prev` as its first argument (enables referential stability patterns)
|
|
60
|
+
- Supports `equals` to suppress downstream propagation when the new value is equivalent
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const fullName = createMemo(() => `${firstName.get()} ${lastName.get()}`)
|
|
64
|
+
```
|
|
65
|
+
</Memo>
|
|
66
|
+
|
|
67
|
+
<Task>
|
|
68
|
+
**What it is:** An asynchronously derived value — like Memo, but async.
|
|
69
|
+
|
|
70
|
+
**Use when:**
|
|
71
|
+
- The derivation requires `await` (data fetching, async transforms, indexed DB reads)
|
|
72
|
+
- You want automatic cancellation of in-flight work when dependencies change
|
|
73
|
+
|
|
74
|
+
**Key facts:**
|
|
75
|
+
- Starts **unset** until the first async operation completes; use `match` for the loading state
|
|
76
|
+
- Receives `(prev, signal: AbortSignal)` — always forward `signal` to `fetch` or any cancellable async operation to prevent stale responses overwriting fresh ones
|
|
77
|
+
- Re-runs automatically when tracked dependencies change, aborting the previous run
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const results = createTask(async (prev, signal) => {
|
|
81
|
+
const res = await fetch(`/api/search?q=${query.get()}`, { signal })
|
|
82
|
+
return res.json()
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
</Task>
|
|
86
|
+
|
|
87
|
+
<Effect>
|
|
88
|
+
**What it is:** A side effect that runs when its tracked dependencies change.
|
|
89
|
+
|
|
90
|
+
**Use when:**
|
|
91
|
+
- You need to synchronise reactive state with the outside world: update the DOM, write to localStorage, send analytics, call an imperative library
|
|
92
|
+
- You need a reactive subscription that runs code (not just derives a value)
|
|
93
|
+
|
|
94
|
+
**Key facts:**
|
|
95
|
+
- **Must be created inside an owner** (`createScope` or another effect) — throws `RequiredOwnerError` otherwise
|
|
96
|
+
- Runs immediately on creation, then re-runs on dependency changes
|
|
97
|
+
- Returns a `Cleanup` function; calling it disposes the effect and all its children
|
|
98
|
+
- Use `unown` inside `connectedCallback` / `disconnectedCallback` when the DOM manages the element's lifetime
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const dispose = createScope(() => {
|
|
102
|
+
createEffect(() => {
|
|
103
|
+
document.title = pageTitle.get()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
// later: dispose()
|
|
107
|
+
```
|
|
108
|
+
</Effect>
|
|
109
|
+
|
|
110
|
+
<Slot>
|
|
111
|
+
**What it is:** A reactive property descriptor — a signal packaged as a getter/setter pair compatible with `Object.defineProperty`.
|
|
112
|
+
|
|
113
|
+
**Use when:**
|
|
114
|
+
- You need to attach a reactive value as a property on an object (e.g. a Web Component's observed attribute)
|
|
115
|
+
- You want property access (`element.name`) to participate in the reactive graph
|
|
116
|
+
|
|
117
|
+
**Key facts:**
|
|
118
|
+
- Has `get`, `set`, `configurable`, and `enumerable` fields
|
|
119
|
+
- Can be passed directly to `Object.defineProperty`
|
|
120
|
+
- Backed by a `State` internally
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const nameSlot = createSlot(store, 'name')
|
|
124
|
+
Object.defineProperty(element, 'name', nameSlot)
|
|
125
|
+
```
|
|
126
|
+
</Slot>
|
|
127
|
+
|
|
128
|
+
<Store>
|
|
129
|
+
**What it is:** A reactive object whose properties are individually reactive.
|
|
130
|
+
|
|
131
|
+
**Use when:**
|
|
132
|
+
- You have a group of related values that are read and updated independently
|
|
133
|
+
- You want fine-grained reactivity on an object's fields rather than replacing the whole object
|
|
134
|
+
|
|
135
|
+
**Key facts:**
|
|
136
|
+
- Reading a property inside an effect creates a dependency on that property only
|
|
137
|
+
- Updating one property does not re-run effects that only read other properties
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const user = createStore({ name: 'Alice', age: 30 })
|
|
141
|
+
user.name = 'Bob' // only effects reading `user.name` re-run
|
|
142
|
+
```
|
|
143
|
+
</Store>
|
|
144
|
+
|
|
145
|
+
<List>
|
|
146
|
+
**What it is:** An ordered, keyed reactive collection — an array where each item has a stable identity.
|
|
147
|
+
|
|
148
|
+
**Use when:**
|
|
149
|
+
- Order matters and items have identity (drag-and-drop lists, ranked results, timelines)
|
|
150
|
+
- You need to react to structural changes (items added, removed, reordered) as well as value changes
|
|
151
|
+
|
|
152
|
+
**Key facts:**
|
|
153
|
+
- Items are identified by a stable key; keys survive sorting and reordering
|
|
154
|
+
- `byKey()`, `at()`, `keyAt()`, and `indexOfKey()` are direct lookups — they **do not create graph edges**
|
|
155
|
+
- To react to structural changes, read `get()`, `keys()`, or `length` instead
|
|
156
|
+
- To update an existing item, use `list.replace(key, value)` — **not** `byKey(key).set(value)`. `replace()` propagates to all subscribers; `byKey().set()` silently misses effects that subscribed via `keys()`, `length`, or the iterator
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const todos = createList(
|
|
160
|
+
[{ id: 't1', text: 'Buy milk', done: false }],
|
|
161
|
+
{ keyConfig: todo => todo.id }
|
|
162
|
+
)
|
|
163
|
+
todos.add({ id: 't2', text: 'Walk dog', done: false })
|
|
164
|
+
todos.replace('t1', { id: 't1', text: 'Buy milk', done: true }) // update in place
|
|
165
|
+
todos.remove('t2')
|
|
166
|
+
```
|
|
167
|
+
</List>
|
|
168
|
+
|
|
169
|
+
<Collection>
|
|
170
|
+
**What it is:** A keyed reactive collection — a reactive Map.
|
|
171
|
+
|
|
172
|
+
**Use when:**
|
|
173
|
+
- Items are identified by key and order is not meaningful or variable
|
|
174
|
+
- You need fast key-based lookup with reactive tracking on the key set and individual entries
|
|
175
|
+
- Use cases: entity caches, normalised data stores, lookup tables
|
|
176
|
+
|
|
177
|
+
**Key facts:**
|
|
178
|
+
- `createCollection` creates a collection from an initial set of entries
|
|
179
|
+
- `deriveCollection` creates a collection derived from another reactive source
|
|
180
|
+
- Same tracking rules as List: `byKey()` does not create graph edges; read `get()`, `keys()`, or `length` to subscribe to structural changes
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const users = createCollection<string, User>(
|
|
184
|
+
existingUsers.map(u => [u.id, u])
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
</Collection>
|
|
188
|
+
|
|
189
|
+
</signal_catalog>
|
|
190
|
+
|
|
191
|
+
<decision_guide>
|
|
192
|
+
|
|
193
|
+
<choose_by_value_source>
|
|
194
|
+
**Who controls the value?**
|
|
195
|
+
|
|
196
|
+
- You set it explicitly → **State**
|
|
197
|
+
- An external event or subscription provides it → **Sensor**
|
|
198
|
+
- It is computed from other signals (sync) → **Memo**
|
|
199
|
+
- It is computed from other signals (async) → **Task**
|
|
200
|
+
</choose_by_value_source>
|
|
201
|
+
|
|
202
|
+
<choose_by_purpose>
|
|
203
|
+
**What do you need to do with it?**
|
|
204
|
+
|
|
205
|
+
- Read a derived value without side effects → **Memo** or **Task**
|
|
206
|
+
- Run a side effect when something changes → **Effect**
|
|
207
|
+
- Expose a reactive value as an object property → **Slot**
|
|
208
|
+
- Group related reactive values on an object → **Store**
|
|
209
|
+
- Maintain an ordered list of keyed items → **List**
|
|
210
|
+
- Maintain an unordered map of keyed items → **Collection**
|
|
211
|
+
</choose_by_purpose>
|
|
212
|
+
|
|
213
|
+
<direct_comparisons>
|
|
214
|
+
|
|
215
|
+
**State vs Sensor**
|
|
216
|
+
Use `State` when you call `.set()` yourself. Use `Sensor` when an external source calls the setter — the library manages the subscription lifecycle via `watched`/`unwatched`.
|
|
217
|
+
|
|
218
|
+
**Memo vs Task**
|
|
219
|
+
Use `Memo` for synchronous derivations. Use `Task` when derivation requires `await`. Both receive `prev` and both support `equals`.
|
|
220
|
+
|
|
221
|
+
**Memo vs Effect**
|
|
222
|
+
`Memo` derives a value (no side effects, lazy). `Effect` runs side effects (imperative, eager, requires owner).
|
|
223
|
+
|
|
224
|
+
**State vs Store**
|
|
225
|
+
Use `State` for a single primitive or object that is always replaced wholesale. Use `Store` for an object whose individual properties are read and updated independently — Store gives you field-level reactivity.
|
|
226
|
+
|
|
227
|
+
**List vs Collection**
|
|
228
|
+
Both are keyed. Use `List` when order is significant (rendering order, ranking, sorting). Use `Collection` when items are looked up by key and order is not meaningful.
|
|
229
|
+
|
|
230
|
+
**List / Collection vs Store**
|
|
231
|
+
Use `Store` for a fixed set of named properties on a single object. Use `List` or `Collection` for a dynamic number of items with uniform shape.
|
|
232
|
+
|
|
233
|
+
</direct_comparisons>
|
|
234
|
+
|
|
235
|
+
</decision_guide>
|
|
236
|
+
|
|
237
|
+
<common_patterns>
|
|
238
|
+
|
|
239
|
+
<loading_state>
|
|
240
|
+
Sensor and Task start unset. Use `match` to handle all states in one expression:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
createEffect(() => {
|
|
244
|
+
match([task], {
|
|
245
|
+
ok: ([data]) => renderData(data),
|
|
246
|
+
err: ([error]) => renderError(error),
|
|
247
|
+
nil: () => renderSpinner(),
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
</loading_state>
|
|
252
|
+
|
|
253
|
+
<grouping_effects>
|
|
254
|
+
Always wrap top-level effects in `createScope` to control their lifetime:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
const dispose = createScope(() => {
|
|
258
|
+
createEffect(() => { /* ... */ })
|
|
259
|
+
createEffect(() => { /* ... */ })
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// When done (e.g. component unmounted):
|
|
263
|
+
dispose()
|
|
264
|
+
```
|
|
265
|
+
</grouping_effects>
|
|
266
|
+
|
|
267
|
+
<coalescing_updates>
|
|
268
|
+
Use `batch` when multiple state writes should trigger only one downstream propagation:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
batch(() => {
|
|
272
|
+
x.set(1)
|
|
273
|
+
y.set(2)
|
|
274
|
+
z.set(3)
|
|
275
|
+
// downstream effects run once, after all three are set
|
|
276
|
+
})
|
|
277
|
+
```
|
|
278
|
+
</coalescing_updates>
|
|
279
|
+
|
|
280
|
+
<reading_without_subscribing>
|
|
281
|
+
Use `untrack` to read a signal's current value without creating a dependency edge:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
createEffect(() => {
|
|
285
|
+
const primary = primary.get() // tracked — re-runs when primary changes
|
|
286
|
+
const snapshot = untrack(() => log.get()) // not tracked — just reads current value
|
|
287
|
+
console.log(primary, snapshot)
|
|
288
|
+
})
|
|
289
|
+
```
|
|
290
|
+
</reading_without_subscribing>
|
|
291
|
+
|
|
292
|
+
</common_patterns>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<required_reading>
|
|
2
|
+
Load references based on question type — only what is needed:
|
|
3
|
+
|
|
4
|
+
- Which signal to use → references/signal-types.md
|
|
5
|
+
- API usage, call signatures, options → references/api-facts.md
|
|
6
|
+
- Unexpected or counterintuitive behavior → references/non-obvious-behaviors.md
|
|
7
|
+
- A thrown error → references/error-classes.md
|
|
8
|
+
- Design rationale or constraints → references/signal-types.md + references/api-facts.md
|
|
9
|
+
</required_reading>
|
|
10
|
+
|
|
11
|
+
<process>
|
|
12
|
+
## Step 1: Categorise the question
|
|
13
|
+
|
|
14
|
+
| Category | Signal words | Load |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Which signal to use | "should I use", "difference between", "when to use", "State vs", "Memo vs" | signal-types.md |
|
|
17
|
+
| API usage / call signature | "how do I", "what does X return", "what are the options for", "how to create" | api-facts.md |
|
|
18
|
+
| Unexpected behavior | "why does this not work", "is this a bug", "why is watched not firing", "not re-running" | non-obvious-behaviors.md |
|
|
19
|
+
| Error meaning | "what does X error mean", "when is Y thrown", "getting RequiredOwnerError" | error-classes.md |
|
|
20
|
+
| Design rationale | "why was X designed this way", "why no null", "why is T extends {}" | signal-types.md + api-facts.md |
|
|
21
|
+
|
|
22
|
+
## Step 2: Load only the relevant references
|
|
23
|
+
|
|
24
|
+
Do not load all references speculatively. Load only the file(s) listed for the identified category.
|
|
25
|
+
|
|
26
|
+
## Step 3: Answer from embedded knowledge
|
|
27
|
+
|
|
28
|
+
All knowledge needed to answer public-API questions is in `references/`. Ground every claim in a reference. Cite the section when the answer is non-obvious (e.g. "per `non-obvious-behaviors.md`…").
|
|
29
|
+
|
|
30
|
+
For counterintuitive behaviors, always show a correct vs incorrect code example alongside the explanation.
|
|
31
|
+
|
|
32
|
+
## Step 4: When embedded knowledge is insufficient
|
|
33
|
+
|
|
34
|
+
If the question requires detail beyond what the references cover (e.g. exact internal flag values, propagation algorithm specifics, architecture decisions), do not guess. Instead:
|
|
35
|
+
- Point to the library's README and GUIDE in the npm package or GitHub repository for public API depth
|
|
36
|
+
- Note that library internals are available in `node_modules/@zeix/cause-effect/src/` if truly needed, but internal details are not part of the public API contract and may change
|
|
37
|
+
|
|
38
|
+
## Step 5: Design rationale questions
|
|
39
|
+
|
|
40
|
+
When asked why the library was designed a certain way, distinguish between:
|
|
41
|
+
- **Hard constraints** (e.g. `T extends {}` excludes null/undefined by design to guarantee signals always have a value; the signal type set is intentionally minimal and closed to new types)
|
|
42
|
+
- **Soft conventions** (naming patterns, file organisation, option defaults)
|
|
43
|
+
|
|
44
|
+
If the distinction is unclear from the embedded references, say so rather than speculating.
|
|
45
|
+
</process>
|
|
46
|
+
|
|
47
|
+
<success_criteria>
|
|
48
|
+
- Answer is grounded in embedded reference knowledge
|
|
49
|
+
- No references to external files that may not exist in the consumer project (REQUIREMENTS.md, ARCHITECTURE.md, src/, etc.)
|
|
50
|
+
- Source cited when the answer is non-obvious
|
|
51
|
+
- Counterintuitive behaviors include correct vs incorrect code examples
|
|
52
|
+
- When knowledge is insufficient, points to upstream documentation rather than guessing
|
|
53
|
+
- No reference files loaded beyond what the question required
|
|
54
|
+
</success_criteria>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<required_reading>
|
|
2
|
+
1. references/non-obvious-behaviors.md — most reactive bugs are documented here
|
|
3
|
+
2. references/error-classes.md — if a specific error is thrown
|
|
4
|
+
3. references/api-facts.md — to verify the signal is being used correctly
|
|
5
|
+
</required_reading>
|
|
6
|
+
|
|
7
|
+
<process>
|
|
8
|
+
## Step 1: Categorise the symptom
|
|
9
|
+
|
|
10
|
+
Match the symptom to the most likely cause before reading any code:
|
|
11
|
+
|
|
12
|
+
| Symptom | Most likely cause | Where to look |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| Effect doesn't re-run when a signal changes | No graph edge established (conditional read or direct lookup) | non-obvious-behaviors: `conditional-reads-delay-watched`, `direct-lookups-do-not-track` |
|
|
15
|
+
| Effect re-runs too often | Missing `equals` option, or mutable reference without `SKIP_EQUALITY` | api-facts: `options.equals`, `SKIP_EQUALITY` |
|
|
16
|
+
| `watched` never fires | Signal only read inside a conditional branch | non-obvious-behaviors: `conditional-reads-delay-watched` |
|
|
17
|
+
| Collection/List update not reflected in effect | Using `byKey()`, `at()`, `keyAt()`, or `indexOfKey()` | non-obvious-behaviors: `direct-lookups-do-not-track` |
|
|
18
|
+
| Stale async result overwrites fresh one | `AbortSignal` not forwarded to `fetch` or async operation | non-obvious-behaviors: `task-abort-on-dependency-change` |
|
|
19
|
+
| `UnsetSignalValueError` thrown | Reading Sensor or Task before first value | non-obvious-behaviors: `sensor-unset-before-first-value` |
|
|
20
|
+
| `RequiredOwnerError` thrown | `createEffect` called outside a `createScope` or parent effect | api-facts: `createEffect`, `createScope` |
|
|
21
|
+
| `CircularDependencyError` thrown | A signal depends on itself, directly or transitively | error-classes: `CircularDependencyError` |
|
|
22
|
+
| Downstream Memo or Effect never updates despite source changing | Custom `equals` on an intermediate Memo is suppressing the subgraph | non-obvious-behaviors: `equals-suppresses-subtrees` |
|
|
23
|
+
| Cleanup not running / memory leak suspected | Effect created without an active owner, or `unown` used incorrectly | api-facts: `unown`, `createScope` |
|
|
24
|
+
|
|
25
|
+
## Step 2: Read the relevant section
|
|
26
|
+
|
|
27
|
+
Read the specific section in references/non-obvious-behaviors.md or references/error-classes.md that matches the symptom. Compare the pattern shown there to the code in question.
|
|
28
|
+
|
|
29
|
+
## Step 3: Verify API usage
|
|
30
|
+
|
|
31
|
+
If the symptom does not match a known gotcha, read references/api-facts.md to confirm:
|
|
32
|
+
- Signal generics use `T extends {}` (no `null` or `undefined`)
|
|
33
|
+
- `createEffect` is inside an owner (`createScope` or another effect)
|
|
34
|
+
- `unown` is used only for DOM-owned lifecycles, not to bypass ownership generally
|
|
35
|
+
- `batch` is used if multiple state writes should coalesce
|
|
36
|
+
|
|
37
|
+
## Step 4: Inspect library source (last resort)
|
|
38
|
+
|
|
39
|
+
If the embedded references do not explain the behavior, the library source is available at:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
node_modules/@zeix/cause-effect/src/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Read the relevant file there — do **not** assume library source files exist at the project root.
|
|
46
|
+
|
|
47
|
+
## Step 5: Fix
|
|
48
|
+
|
|
49
|
+
Apply the minimal change that addresses the root cause. Do not suppress the symptom (e.g. wrapping a read in `untrack`) without first confirming that is the intended behavior.
|
|
50
|
+
|
|
51
|
+
## Step 6: Verify
|
|
52
|
+
|
|
53
|
+
Run the project's own test suite:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# use whichever applies to this project
|
|
57
|
+
npm test
|
|
58
|
+
pnpm test
|
|
59
|
+
yarn test
|
|
60
|
+
npx vitest
|
|
61
|
+
npx jest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Do not assume `bun` or any specific test runner is available.
|
|
65
|
+
</process>
|
|
66
|
+
|
|
67
|
+
<success_criteria>
|
|
68
|
+
- Root cause identified, not symptom suppressed
|
|
69
|
+
- Fix matches the correct pattern from references/non-obvious-behaviors.md where applicable
|
|
70
|
+
- Project test suite passes after the fix
|
|
71
|
+
</success_criteria>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<required_reading>
|
|
2
|
+
1. references/signal-types.md — choose the right signal type(s) for the task
|
|
3
|
+
2. references/api-facts.md — correct API usage, constraints, and callback patterns
|
|
4
|
+
3. references/non-obvious-behaviors.md — avoid common pitfalls before writing any code
|
|
5
|
+
</required_reading>
|
|
6
|
+
|
|
7
|
+
<process>
|
|
8
|
+
## Step 1: Choose the right signal type
|
|
9
|
+
|
|
10
|
+
Read references/signal-types.md. Match the task to the appropriate signal(s) using the decision guide. If multiple signal types seem applicable, the guide has explicit comparisons.
|
|
11
|
+
|
|
12
|
+
## Step 2: Check ownership requirements
|
|
13
|
+
|
|
14
|
+
Before writing any `createEffect` call, confirm there is an active owner in scope:
|
|
15
|
+
- Top-level effects must be wrapped in `createScope`
|
|
16
|
+
- Effects in Web Component lifecycle methods that use DOM-managed lifetime should use `unown`
|
|
17
|
+
|
|
18
|
+
See references/api-facts.md → `<core_functions>` for the rules.
|
|
19
|
+
|
|
20
|
+
## Step 3: Check for known pitfalls
|
|
21
|
+
|
|
22
|
+
Skim references/non-obvious-behaviors.md for anything that applies to the task:
|
|
23
|
+
|
|
24
|
+
| If the task involves… | Check… |
|
|
25
|
+
|---|---|
|
|
26
|
+
| List or Collection | direct-lookups-do-not-track |
|
|
27
|
+
| Conditional rendering or `match` | conditional-reads-delay-watched |
|
|
28
|
+
| Async data fetching | task-abort-on-dependency-change |
|
|
29
|
+
| Sensor or Task before first value | sensor-unset-before-first-value |
|
|
30
|
+
| Multiple state updates at once | references/api-facts.md → `batch` |
|
|
31
|
+
|
|
32
|
+
## Step 4: Import what you need
|
|
33
|
+
|
|
34
|
+
All public API is imported from the package root:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import {
|
|
38
|
+
createState, createSensor, createMemo, createTask,
|
|
39
|
+
createEffect, createScope, createSlot, createStore,
|
|
40
|
+
createList, createCollection, deriveCollection,
|
|
41
|
+
batch, untrack, unown, match,
|
|
42
|
+
SKIP_EQUALITY,
|
|
43
|
+
} from '@zeix/cause-effect'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Only import what the task requires.
|
|
47
|
+
|
|
48
|
+
## Step 5: Implement
|
|
49
|
+
|
|
50
|
+
Write the code following the patterns in references/. Prefer the examples shown there over inventing new patterns — the non-obvious behaviors exist precisely because intuitive patterns are often wrong.
|
|
51
|
+
|
|
52
|
+
## Step 6: Verify
|
|
53
|
+
|
|
54
|
+
Run the project's own test suite using whatever command applies to this project (e.g. `npm test`, `pnpm test`, `npx vitest`, `deno test`). Do not assume a specific test runner or package manager.
|
|
55
|
+
</process>
|
|
56
|
+
|
|
57
|
+
<success_criteria>
|
|
58
|
+
- Correct signal type(s) chosen for the task
|
|
59
|
+
- No `null` or `undefined` in signal generics (`T extends {}`)
|
|
60
|
+
- Every `createEffect` is inside a `createScope` or another effect
|
|
61
|
+
- Code follows the patterns from references/ rather than inventing new ones
|
|
62
|
+
- Project's own test suite passes (if applicable)
|
|
63
|
+
</success_criteria>
|
|
@@ -7,108 +7,69 @@ description: >
|
|
|
7
7
|
user_invocable: false
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
<scope>
|
|
11
|
+
This skill is for development work **on the @zeix/cause-effect library itself** — use it only inside the cause-effect repository where `REQUIREMENTS.md`, `ARCHITECTURE.md`, and `src/` are present at the project root.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
For consumer projects that use `@zeix/cause-effect` as a dependency, use the `cause-effect` skill instead.
|
|
14
|
+
</scope>
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
<essential_principles>
|
|
17
|
+
**Read before writing.** Always read the relevant source file(s) before proposing or making changes.
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
**The signal type set is complete.** Check `REQUIREMENTS.md` before proposing anything new — new signal types are explicitly out of scope.
|
|
20
|
+
|
|
21
|
+
**`T extends {}`** — all signal generics exclude `null` and `undefined`. Use wrapper types or sentinel values to represent absence.
|
|
22
|
+
|
|
23
|
+
**Run `bun test`** after every change.
|
|
24
|
+
</essential_principles>
|
|
25
|
+
|
|
26
|
+
<intake>
|
|
27
|
+
What kind of task is this?
|
|
28
|
+
|
|
29
|
+
1. **Implement** — add or extend functionality
|
|
30
|
+
2. **Fix** — debug or fix unexpected behavior
|
|
31
|
+
3. **Test** — write or update tests
|
|
32
|
+
4. **Question** — understand the API, internals, or a design decision
|
|
33
|
+
|
|
34
|
+
**Wait for response before proceeding.**
|
|
35
|
+
</intake>
|
|
36
|
+
|
|
37
|
+
<routing>
|
|
38
|
+
| Response | Workflow |
|
|
17
39
|
|---|---|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
|
36
|
-
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
StateNode<T> — source only
|
|
51
|
-
MemoNode<T> — source + sink (also used by Slot, Store, List, Collection internals)
|
|
52
|
-
TaskNode<T> — source + sink + AbortController
|
|
53
|
-
EffectNode — sink + owner
|
|
54
|
-
Scope — owner only
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Two independent global pointers:
|
|
58
|
-
- `activeSink` — tracked for dependency edges (nulled by `untrack()`)
|
|
59
|
-
- `activeOwner` — tracked for cleanup registration (nulled by `unown()`)
|
|
60
|
-
|
|
61
|
-
## Key API Facts
|
|
62
|
-
|
|
63
|
-
- **`T extends {}`** — all signal generics exclude `null` and `undefined`. Use wrapper types or sentinel values to represent absence.
|
|
64
|
-
- **`createScope(fn)`** — returns a single `Cleanup` function. `fn` receives no arguments and returns an optional cleanup.
|
|
65
|
-
- **`createEffect(fn)`** — returns a `Cleanup`. Must be called inside an owner (effect or scope).
|
|
66
|
-
- **`batch(fn)`** — defers flush until `fn` returns; multiple state writes coalesce into one propagation.
|
|
67
|
-
- **`untrack(fn)`** — runs `fn` without recording dependency edges (nulls `activeSink`).
|
|
68
|
-
- **`unown(fn)`** — runs `fn` without registering cleanup in the current owner (nulls `activeOwner`). Use in `connectedCallback` for DOM-owned lifecycles.
|
|
69
|
-
- **`SKIP_EQUALITY`** — sentinel for `options.equals`; forces propagation on every update (use with mutable-reference sensors).
|
|
70
|
-
- **Memo/Task callbacks receive `prev`** — the previous value as first argument, enabling reducer patterns without external state.
|
|
71
|
-
- **`Slot` is a property descriptor** — has `get`, `set`, `configurable`, `enumerable`; can be passed directly to `Object.defineProperty()`.
|
|
72
|
-
|
|
73
|
-
## Non-Obvious Behaviors
|
|
74
|
-
|
|
75
|
-
**`byKey()`, `at()`, `keyAt()`, `indexOfKey()` do not create graph edges.** They are direct lookups. To react to structural changes (key added/removed), read `get()`, `keys()`, or `length`.
|
|
76
|
-
|
|
77
|
-
**Conditional reads delay `watched` activation.** Read signals eagerly before conditional logic to ensure `watched` fires immediately:
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
// Good — both signals tracked on every run
|
|
81
|
-
createEffect(() => {
|
|
82
|
-
match([task, derived], { ok: ([r, v]) => render(v, r), nil: () => showSpinner() })
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
// Bad — derived only tracked after task resolves
|
|
86
|
-
createEffect(() => {
|
|
87
|
-
match([task], { ok: ([r]) => render(derived.get(), r), nil: () => showSpinner() })
|
|
88
|
-
})
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**`equals` suppresses entire subtrees.** When a Memo recomputes to the same value, downstream nodes receive `FLAG_CHECK` and are skipped without running. A custom `equals` on an intermediate Memo can suppress whole subgraphs.
|
|
92
|
-
|
|
93
|
-
**`watched` stays stable through mutations.** Structural mutations on a List/Collection source do not restart the `watched` callback; it stays active as long as any downstream effect is subscribed.
|
|
94
|
-
|
|
95
|
-
## Error Classes
|
|
96
|
-
|
|
97
|
-
| Class | When thrown |
|
|
40
|
+
| 1, "implement", "add", "extend", "build" | workflows/implement-feature.md |
|
|
41
|
+
| 2, "fix", "bug", "debug", "broken", "wrong" | workflows/fix-bug.md |
|
|
42
|
+
| 3, "test", "spec", "coverage" | workflows/write-tests.md |
|
|
43
|
+
| 4, "question", "explain", "how", "why", "what" | workflows/answer-question.md |
|
|
44
|
+
|
|
45
|
+
**Intent-based routing** (if user provides clear context without selecting):
|
|
46
|
+
- Describes a change to make → workflows/implement-feature.md
|
|
47
|
+
- Describes something not working → workflows/fix-bug.md
|
|
48
|
+
- Asks to write/update tests → workflows/write-tests.md
|
|
49
|
+
- Asks how something works → workflows/answer-question.md
|
|
50
|
+
|
|
51
|
+
**After identifying the workflow, read it and follow it exactly.**
|
|
52
|
+
</routing>
|
|
53
|
+
|
|
54
|
+
<reference_index>
|
|
55
|
+
All in `references/`:
|
|
56
|
+
|
|
57
|
+
| File | Contents |
|
|
58
|
+
|---|---|
|
|
59
|
+
| source-map.md | Authoritative documents + signal source file locations |
|
|
60
|
+
| internal-types.md | Node shapes and global pointers |
|
|
61
|
+
| api-facts.md | Key API constraints and callback patterns |
|
|
62
|
+
| non-obvious-behaviors.md | Counterintuitive behaviors with examples |
|
|
63
|
+
| error-classes.md | Error classes and when they are thrown |
|
|
64
|
+
</reference_index>
|
|
65
|
+
|
|
66
|
+
<workflows_index>
|
|
67
|
+
All in `workflows/`:
|
|
68
|
+
|
|
69
|
+
| Workflow | Purpose |
|
|
98
70
|
|---|---|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
104
|
-
| `ReadonlySignalError` | Writing to a read-only signal |
|
|
105
|
-
| `RequiredOwnerError` | `createEffect` called outside an owner |
|
|
106
|
-
| `CircularDependencyError` | Cycle detected in the graph |
|
|
107
|
-
|
|
108
|
-
## Workflow
|
|
109
|
-
|
|
110
|
-
1. **Read before writing.** Always read the relevant source file(s) before proposing or making changes.
|
|
111
|
-
2. **Check `ARCHITECTURE.md`** for graph-level questions (propagation, ownership, flag semantics).
|
|
112
|
-
3. **Check `README.md`** for public API usage patterns and option signatures.
|
|
113
|
-
4. **Check `REQUIREMENTS.md`** before adding anything new — the signal type set is complete and new types are explicitly out of scope.
|
|
114
|
-
5. **Run `bun test`** after changes to verify correctness.
|
|
71
|
+
| implement-feature.md | Add or extend library functionality |
|
|
72
|
+
| fix-bug.md | Diagnose and fix unexpected behavior |
|
|
73
|
+
| write-tests.md | Write or update tests for a signal type or behavior |
|
|
74
|
+
| answer-question.md | Answer questions about the API, internals, or design |
|
|
75
|
+
</workflows_index>
|