@zeix/cause-effect 0.18.5 → 1.0.1

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 (63) hide show
  1. package/.github/copilot-instructions.md +2 -1
  2. package/.zed/settings.json +24 -1
  3. package/ARCHITECTURE.md +2 -2
  4. package/CHANGELOG.md +23 -0
  5. package/README.md +42 -1
  6. package/REQUIREMENTS.md +3 -3
  7. package/bench/reactivity.bench.ts +18 -7
  8. package/biome.json +1 -1
  9. package/eslint.config.js +2 -1
  10. package/index.dev.js +11 -5
  11. package/index.js +1 -1
  12. package/index.ts +1 -1
  13. package/package.json +6 -6
  14. package/skills/cause-effect/SKILL.md +69 -0
  15. package/skills/cause-effect/agents/openai.yaml +4 -0
  16. package/skills/cause-effect/references/api-facts.md +179 -0
  17. package/skills/cause-effect/references/error-classes.md +153 -0
  18. package/skills/cause-effect/references/non-obvious-behaviors.md +173 -0
  19. package/skills/cause-effect/references/signal-types.md +288 -0
  20. package/skills/cause-effect/workflows/answer-question.md +54 -0
  21. package/skills/cause-effect/workflows/debug.md +71 -0
  22. package/skills/cause-effect/workflows/use-api.md +63 -0
  23. package/skills/cause-effect-dev/SKILL.md +75 -0
  24. package/skills/cause-effect-dev/agents/openai.yaml +4 -0
  25. package/skills/cause-effect-dev/references/api-facts.md +96 -0
  26. package/skills/cause-effect-dev/references/error-classes.md +97 -0
  27. package/skills/cause-effect-dev/references/internal-types.md +54 -0
  28. package/skills/cause-effect-dev/references/non-obvious-behaviors.md +146 -0
  29. package/skills/cause-effect-dev/references/source-map.md +45 -0
  30. package/skills/cause-effect-dev/workflows/answer-question.md +55 -0
  31. package/skills/cause-effect-dev/workflows/fix-bug.md +63 -0
  32. package/skills/cause-effect-dev/workflows/implement-feature.md +46 -0
  33. package/skills/cause-effect-dev/workflows/write-tests.md +64 -0
  34. package/skills/changelog-keeper/SKILL.md +47 -37
  35. package/skills/tech-writer/SKILL.md +94 -0
  36. package/skills/tech-writer/references/document-map.md +199 -0
  37. package/skills/tech-writer/references/tone-guide.md +189 -0
  38. package/skills/tech-writer/workflows/consistency-review.md +98 -0
  39. package/skills/tech-writer/workflows/update-after-change.md +65 -0
  40. package/skills/tech-writer/workflows/update-agent-docs.md +77 -0
  41. package/skills/tech-writer/workflows/update-architecture.md +61 -0
  42. package/skills/tech-writer/workflows/update-jsdoc.md +72 -0
  43. package/skills/tech-writer/workflows/update-public-api.md +59 -0
  44. package/skills/tech-writer/workflows/update-requirements.md +80 -0
  45. package/src/graph.ts +8 -4
  46. package/src/nodes/collection.ts +42 -2
  47. package/src/nodes/effect.ts +13 -1
  48. package/src/nodes/list.ts +28 -4
  49. package/src/nodes/memo.ts +0 -1
  50. package/src/nodes/sensor.ts +10 -4
  51. package/src/nodes/store.ts +11 -0
  52. package/src/signal.ts +6 -0
  53. package/test/benchmark.test.ts +25 -11
  54. package/test/collection.test.ts +6 -3
  55. package/test/effect.test.ts +2 -1
  56. package/test/list.test.ts +8 -4
  57. package/test/regression.test.ts +4 -2
  58. package/test/store.test.ts +8 -4
  59. package/test/util/dependency-graph.ts +12 -6
  60. package/tsconfig.json +14 -1
  61. package/types/index.d.ts +1 -1
  62. package/types/src/graph.d.ts +2 -2
  63. package/OWNERSHIP_BUG.md +0 -95
@@ -0,0 +1,288 @@
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 key; keys must be unique
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
+
157
+ ```typescript
158
+ const todos = createList<Todo, 'id'>('id', [
159
+ { id: '1', text: 'Buy milk', done: false },
160
+ ])
161
+ todos.push({ id: '2', text: 'Walk dog', done: false })
162
+ ```
163
+ </List>
164
+
165
+ <Collection>
166
+ **What it is:** A keyed reactive collection — a reactive Map.
167
+
168
+ **Use when:**
169
+ - Items are identified by key and order is not meaningful or variable
170
+ - You need fast key-based lookup with reactive tracking on the key set and individual entries
171
+ - Use cases: entity caches, normalised data stores, lookup tables
172
+
173
+ **Key facts:**
174
+ - `createCollection` creates a collection from an initial set of entries
175
+ - `deriveCollection` creates a collection derived from another reactive source
176
+ - Same tracking rules as List: `byKey()` does not create graph edges; read `get()`, `keys()`, or `length` to subscribe to structural changes
177
+
178
+ ```typescript
179
+ const users = createCollection<string, User>(
180
+ existingUsers.map(u => [u.id, u])
181
+ )
182
+ ```
183
+ </Collection>
184
+
185
+ </signal_catalog>
186
+
187
+ <decision_guide>
188
+
189
+ <choose_by_value_source>
190
+ **Who controls the value?**
191
+
192
+ - You set it explicitly → **State**
193
+ - An external event or subscription provides it → **Sensor**
194
+ - It is computed from other signals (sync) → **Memo**
195
+ - It is computed from other signals (async) → **Task**
196
+ </choose_by_value_source>
197
+
198
+ <choose_by_purpose>
199
+ **What do you need to do with it?**
200
+
201
+ - Read a derived value without side effects → **Memo** or **Task**
202
+ - Run a side effect when something changes → **Effect**
203
+ - Expose a reactive value as an object property → **Slot**
204
+ - Group related reactive values on an object → **Store**
205
+ - Maintain an ordered list of keyed items → **List**
206
+ - Maintain an unordered map of keyed items → **Collection**
207
+ </choose_by_purpose>
208
+
209
+ <direct_comparisons>
210
+
211
+ **State vs Sensor**
212
+ 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`.
213
+
214
+ **Memo vs Task**
215
+ Use `Memo` for synchronous derivations. Use `Task` when derivation requires `await`. Both receive `prev` and both support `equals`.
216
+
217
+ **Memo vs Effect**
218
+ `Memo` derives a value (no side effects, lazy). `Effect` runs side effects (imperative, eager, requires owner).
219
+
220
+ **State vs Store**
221
+ 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.
222
+
223
+ **List vs Collection**
224
+ 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.
225
+
226
+ **List / Collection vs Store**
227
+ 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.
228
+
229
+ </direct_comparisons>
230
+
231
+ </decision_guide>
232
+
233
+ <common_patterns>
234
+
235
+ <loading_state>
236
+ Sensor and Task start unset. Use `match` to handle all states in one expression:
237
+
238
+ ```typescript
239
+ createEffect(() => {
240
+ match([task], {
241
+ ok: ([data]) => renderData(data),
242
+ err: ([error]) => renderError(error),
243
+ nil: () => renderSpinner(),
244
+ })
245
+ })
246
+ ```
247
+ </loading_state>
248
+
249
+ <grouping_effects>
250
+ Always wrap top-level effects in `createScope` to control their lifetime:
251
+
252
+ ```typescript
253
+ const dispose = createScope(() => {
254
+ createEffect(() => { /* ... */ })
255
+ createEffect(() => { /* ... */ })
256
+ })
257
+
258
+ // When done (e.g. component unmounted):
259
+ dispose()
260
+ ```
261
+ </grouping_effects>
262
+
263
+ <coalescing_updates>
264
+ Use `batch` when multiple state writes should trigger only one downstream propagation:
265
+
266
+ ```typescript
267
+ batch(() => {
268
+ x.set(1)
269
+ y.set(2)
270
+ z.set(3)
271
+ // downstream effects run once, after all three are set
272
+ })
273
+ ```
274
+ </coalescing_updates>
275
+
276
+ <reading_without_subscribing>
277
+ Use `untrack` to read a signal's current value without creating a dependency edge:
278
+
279
+ ```typescript
280
+ createEffect(() => {
281
+ const primary = primary.get() // tracked — re-runs when primary changes
282
+ const snapshot = untrack(() => log.get()) // not tracked — just reads current value
283
+ console.log(primary, snapshot)
284
+ })
285
+ ```
286
+ </reading_without_subscribing>
287
+
288
+ </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>
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: cause-effect-dev
3
+ description: >
4
+ Expert developer for the @zeix/cause-effect reactive signals library. Use when
5
+ implementing features, fixing bugs, writing tests, or answering questions about
6
+ the library's internals, public API, or design decisions.
7
+ user_invocable: false
8
+ ---
9
+
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.
12
+
13
+ For consumer projects that use `@zeix/cause-effect` as a dependency, use the `cause-effect` skill instead.
14
+ </scope>
15
+
16
+ <essential_principles>
17
+ **Read before writing.** Always read the relevant source file(s) before proposing or making changes.
18
+
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 |
39
+ |---|---|
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 |
70
+ |---|---|
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>
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Cause & Effect Developer"
3
+ short_description: "Expert developer for the @zeix/cause-effect reactive signals library"
4
+ default_prompt: "Use $cause-effect-dev to implement, debug, or explain code using the @zeix/cause-effect library."
@@ -0,0 +1,96 @@
1
+ <overview>
2
+ Key API constraints, defaults, and callback patterns for @zeix/cause-effect. Read this when writing or reviewing any code that uses the public API.
3
+ </overview>
4
+
5
+ <type_constraint>
6
+ **`T extends {}`** — all signal generics exclude `null` and `undefined` at the type level. This is intentional. Use wrapper types or sentinel values to represent absence:
7
+
8
+ ```typescript
9
+ // Wrong — TypeScript will reject this
10
+ const count = createState<number | null>(null)
11
+
12
+ // Correct — use a sentinel or wrapper
13
+ const count = createState<number>(0)
14
+ const value = createState<{ data: string } | never>({ data: '' })
15
+ ```
16
+ </type_constraint>
17
+
18
+ <core_functions>
19
+ **`createScope(fn)`**
20
+ - Returns a single `Cleanup` function
21
+ - `fn` receives no arguments
22
+ - `fn` may return an optional cleanup that runs when the scope is disposed
23
+ - Used to group effects and control their lifetime
24
+
25
+ **`createEffect(fn)`**
26
+ - Returns a `Cleanup` function
27
+ - **Must be called inside an owner** (another effect or a scope) — throws `RequiredOwnerError` otherwise
28
+ - `fn` runs immediately and re-runs whenever its tracked dependencies change
29
+ - Registers cleanup with the current `activeOwner`
30
+
31
+ **`batch(fn)`**
32
+ - Defers the reactive flush until `fn` returns
33
+ - Multiple state writes inside `fn` coalesce into a single propagation pass
34
+ - Use when updating several signals that feed the same downstream computation
35
+
36
+ **`untrack(fn)`**
37
+ - Runs `fn` without recording dependency edges (nulls `activeSink`)
38
+ - Reads inside `fn` do not subscribe the current computation to those signals
39
+ - Use to read a signal's current value without creating a reactive dependency
40
+
41
+ **`unown(fn)`**
42
+ - Runs `fn` without registering cleanups in the current owner (nulls `activeOwner`)
43
+ - Use in `connectedCallback` and similar DOM lifecycle hooks where the DOM — not the reactive graph — owns the element's lifetime
44
+ </core_functions>
45
+
46
+ <options>
47
+ **`equals`**
48
+ - Available on `createState`, `createSensor`, `createMemo`, `createTask`
49
+ - Default: strict equality (`===`)
50
+ - When a new value is `equals` to the previous, propagation stops — downstream nodes are not re-run
51
+ - **`SKIP_EQUALITY`** — special sentinel for `equals`; forces propagation on every update regardless of value. Use with mutable-reference sensors where the reference never changes but the contents do
52
+
53
+ **`guard`**
54
+ - Available on `createState`, `createSensor`
55
+ - A predicate `(value: unknown) => value is T`
56
+ - Throws `InvalidSignalValueError` if a set value fails the guard
57
+ - Use to enforce runtime type safety at signal boundaries
58
+ </options>
59
+
60
+ <callback_patterns>
61
+ **Memo and Task callbacks receive `prev`**
62
+ - Signature: `(prev: T) => T` for Memo; `(prev: T, signal: AbortSignal) => Promise<T>` for Task
63
+ - `prev` is the previous value on every run after the first
64
+ - Enables reducer patterns without external state:
65
+
66
+ ```typescript
67
+ const count = createState(0)
68
+ const doubled = createMemo((prev) => {
69
+ const next = count.get() * 2
70
+ return next === prev ? prev : next // referential stability
71
+ })
72
+ ```
73
+
74
+ **`Slot` is a property descriptor**
75
+ - Has `get`, `set`, `configurable`, `enumerable` fields
76
+ - Can be passed directly to `Object.defineProperty()`:
77
+
78
+ ```typescript
79
+ const slot = createSlot(store, 'name')
80
+ Object.defineProperty(element, 'name', slot)
81
+ ```
82
+
83
+ **`Task` carries an `AbortSignal`**
84
+ - The second argument to the Task callback is an `AbortSignal`
85
+ - The signal is aborted when the Task's dependencies change before the previous async operation completes
86
+ - Always pass it to any `fetch` or cancellable async operation inside a Task
87
+ </callback_patterns>
88
+
89
+ <lifecycle_summary>
90
+ | Function | Must be in owner? | Returns | Re-runs on dependency change? |
91
+ |---|---|---|---|
92
+ | `createScope(fn)` | No | `Cleanup` | No (fn runs once) |
93
+ | `createEffect(fn)` | **Yes** | `Cleanup` | Yes |
94
+ | `createMemo(fn)` | No | `Memo<T>` | Lazily (on read) |
95
+ | `createTask(fn)` | No | `Task<T>` | Yes (async) |
96
+ </lifecycle_summary>