@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.
Files changed (62) hide show
  1. package/.github/copilot-instructions.md +5 -1
  2. package/.zed/settings.json +24 -1
  3. package/ARCHITECTURE.md +27 -1
  4. package/CHANGELOG.md +21 -0
  5. package/CLAUDE.md +1 -1
  6. package/GUIDE.md +2 -5
  7. package/README.md +45 -3
  8. package/REQUIREMENTS.md +3 -3
  9. package/eslint.config.js +2 -1
  10. package/index.dev.js +14 -0
  11. package/index.js +1 -1
  12. package/index.ts +1 -1
  13. package/package.json +5 -4
  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 +195 -0
  19. package/skills/cause-effect/references/signal-types.md +292 -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 +61 -100
  24. package/skills/cause-effect-dev/references/api-facts.md +96 -0
  25. package/skills/cause-effect-dev/references/error-classes.md +97 -0
  26. package/skills/cause-effect-dev/references/internal-types.md +54 -0
  27. package/skills/cause-effect-dev/references/non-obvious-behaviors.md +162 -0
  28. package/skills/cause-effect-dev/references/source-map.md +45 -0
  29. package/skills/cause-effect-dev/workflows/answer-question.md +55 -0
  30. package/skills/cause-effect-dev/workflows/fix-bug.md +63 -0
  31. package/skills/cause-effect-dev/workflows/implement-feature.md +46 -0
  32. package/skills/cause-effect-dev/workflows/write-tests.md +64 -0
  33. package/skills/changelog-keeper/SKILL.md +47 -37
  34. package/skills/tech-writer/SKILL.md +94 -0
  35. package/skills/tech-writer/references/document-map.md +199 -0
  36. package/skills/tech-writer/references/tone-guide.md +189 -0
  37. package/skills/tech-writer/workflows/consistency-review.md +98 -0
  38. package/skills/tech-writer/workflows/update-after-change.md +65 -0
  39. package/skills/tech-writer/workflows/update-agent-docs.md +77 -0
  40. package/skills/tech-writer/workflows/update-architecture.md +61 -0
  41. package/skills/tech-writer/workflows/update-jsdoc.md +72 -0
  42. package/skills/tech-writer/workflows/update-public-api.md +59 -0
  43. package/skills/tech-writer/workflows/update-requirements.md +80 -0
  44. package/src/graph.ts +2 -0
  45. package/src/nodes/collection.ts +38 -0
  46. package/src/nodes/effect.ts +13 -1
  47. package/src/nodes/list.ts +41 -2
  48. package/src/nodes/memo.ts +0 -1
  49. package/src/nodes/sensor.ts +10 -4
  50. package/src/nodes/store.ts +11 -0
  51. package/src/signal.ts +6 -0
  52. package/test/list.test.ts +121 -0
  53. package/tsconfig.json +9 -0
  54. package/types/index.d.ts +1 -1
  55. package/types/src/graph.d.ts +2 -0
  56. package/types/src/nodes/collection.d.ts +38 -0
  57. package/types/src/nodes/effect.d.ts +13 -1
  58. package/types/src/nodes/list.d.ts +30 -2
  59. package/types/src/nodes/memo.d.ts +0 -1
  60. package/types/src/nodes/sensor.d.ts +10 -4
  61. package/types/src/nodes/store.d.ts +11 -0
  62. 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
- # Cause & Effect — Developer Skill
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
- You are an expert developer on the **@zeix/cause-effect** reactive state management primitives library (v1.0.0). Work only from authoritative sources listed below. Never guess API shapes or behaviors — read the source.
13
+ For consumer projects that use `@zeix/cause-effect` as a dependency, use the `cause-effect` skill instead.
14
+ </scope>
13
15
 
14
- ## Authoritative Sources
16
+ <essential_principles>
17
+ **Read before writing.** Always read the relevant source file(s) before proposing or making changes.
15
18
 
16
- | What you need | Where to look |
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
- | Vision, audience, constraints, non-goals | `REQUIREMENTS.md` |
19
- | Mental model, non-obvious behaviors, TS constraints | `CLAUDE.md` |
20
- | Full API reference with examples | `README.md` |
21
- | Mapping from React/Vue/Angular patterns; when to use each signal type | `GUIDE.md` |
22
- | Graph engine architecture, node shapes, propagation | `ARCHITECTURE.md` |
23
- | Public API surface (all exports, types) | `index.ts` |
24
- | Core graph engine (flags, propagation, flush, ownership) | `src/graph.ts` |
25
- | Error classes | `src/errors.ts` |
26
- | Signal base types and type guards | `src/signal.ts` |
27
- | Shared utilities | `src/util.ts` |
28
-
29
- ## Source File Map
30
-
31
- Each signal type lives in its own file:
32
-
33
- | Signal | File | Create | Type guard |
34
- |---|---|---|---|
35
- | State | `src/nodes/state.ts` | `createState()` | `isState()` |
36
- | Sensor | `src/nodes/sensor.ts` | `createSensor()` | `isSensor()` |
37
- | Memo | `src/nodes/memo.ts` | `createMemo()` | `isMemo()` |
38
- | Task | `src/nodes/task.ts` | `createTask()` | `isTask()` |
39
- | Effect | `src/nodes/effect.ts` | `createEffect()` | |
40
- | Slot | `src/nodes/slot.ts` | `createSlot()` | `isSlot()` |
41
- | Store | `src/nodes/store.ts` | `createStore()` | `isStore()` |
42
- | List | `src/nodes/list.ts` | `createList()` | `isList()` |
43
- | Collection | `src/nodes/collection.ts` | `createCollection()` / `deriveCollection()` | `isCollection()` |
44
-
45
- `match()` and `MatchHandlers` live in `src/nodes/effect.ts` alongside `createEffect`.
46
-
47
- ## Internal Node Shapes
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
- | `NullishSignalValueError` | Signal value is `null` or `undefined` |
100
- | `InvalidSignalValueError` | Value fails the `guard` check |
101
- | `InvalidCallbackError` | A required callback argument is not a function |
102
- | `DuplicateKeyError` | List/Collection key collision |
103
- | `UnsetSignalValueError` | Reading a Sensor/Task before it has produced a value |
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>