@zeix/cause-effect 1.0.0 → 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 (47) hide show
  1. package/.github/copilot-instructions.md +2 -1
  2. package/.zed/settings.json +24 -1
  3. package/ARCHITECTURE.md +1 -1
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +41 -0
  6. package/REQUIREMENTS.md +3 -3
  7. package/eslint.config.js +2 -1
  8. package/package.json +5 -4
  9. package/skills/cause-effect/SKILL.md +69 -0
  10. package/skills/cause-effect/agents/openai.yaml +4 -0
  11. package/skills/cause-effect/references/api-facts.md +179 -0
  12. package/skills/cause-effect/references/error-classes.md +153 -0
  13. package/skills/cause-effect/references/non-obvious-behaviors.md +173 -0
  14. package/skills/cause-effect/references/signal-types.md +288 -0
  15. package/skills/cause-effect/workflows/answer-question.md +54 -0
  16. package/skills/cause-effect/workflows/debug.md +71 -0
  17. package/skills/cause-effect/workflows/use-api.md +63 -0
  18. package/skills/cause-effect-dev/SKILL.md +61 -100
  19. package/skills/cause-effect-dev/references/api-facts.md +96 -0
  20. package/skills/cause-effect-dev/references/error-classes.md +97 -0
  21. package/skills/cause-effect-dev/references/internal-types.md +54 -0
  22. package/skills/cause-effect-dev/references/non-obvious-behaviors.md +146 -0
  23. package/skills/cause-effect-dev/references/source-map.md +45 -0
  24. package/skills/cause-effect-dev/workflows/answer-question.md +55 -0
  25. package/skills/cause-effect-dev/workflows/fix-bug.md +63 -0
  26. package/skills/cause-effect-dev/workflows/implement-feature.md +46 -0
  27. package/skills/cause-effect-dev/workflows/write-tests.md +64 -0
  28. package/skills/changelog-keeper/SKILL.md +47 -37
  29. package/skills/tech-writer/SKILL.md +94 -0
  30. package/skills/tech-writer/references/document-map.md +199 -0
  31. package/skills/tech-writer/references/tone-guide.md +189 -0
  32. package/skills/tech-writer/workflows/consistency-review.md +98 -0
  33. package/skills/tech-writer/workflows/update-after-change.md +65 -0
  34. package/skills/tech-writer/workflows/update-agent-docs.md +77 -0
  35. package/skills/tech-writer/workflows/update-architecture.md +61 -0
  36. package/skills/tech-writer/workflows/update-jsdoc.md +72 -0
  37. package/skills/tech-writer/workflows/update-public-api.md +59 -0
  38. package/skills/tech-writer/workflows/update-requirements.md +80 -0
  39. package/src/graph.ts +2 -0
  40. package/src/nodes/collection.ts +38 -0
  41. package/src/nodes/effect.ts +13 -1
  42. package/src/nodes/list.ts +23 -2
  43. package/src/nodes/memo.ts +0 -1
  44. package/src/nodes/sensor.ts +10 -4
  45. package/src/nodes/store.ts +11 -0
  46. package/src/signal.ts +6 -0
  47. package/tsconfig.json +9 -0
@@ -0,0 +1,173 @@
1
+ <overview>
2
+ Counterintuitive behaviors in @zeix/cause-effect that commonly cause bugs or confusion.
3
+ All knowledge is self-contained — no library source files required. Read this when debugging
4
+ unexpected reactive behavior, or when writing code that involves collections, conditional
5
+ reads, async operations, or ownership.
6
+ </overview>
7
+
8
+ <direct_lookups_do_not_track>
9
+ **`byKey()`, `at()`, `keyAt()`, and `indexOfKey()` do not create graph edges.** They are
10
+ direct lookups into the internal map/array — calling them inside an effect or memo does not
11
+ subscribe to structural changes.
12
+
13
+ To react to structural changes (key added, key removed, order changed), read a tracking
14
+ accessor instead:
15
+
16
+ | You want to react to | Read this |
17
+ |---|---|
18
+ | Any structural change | `collection.get()` or `list.get()` |
19
+ | Key set membership | `collection.keys()` |
20
+ | Length / item count | `collection.length` |
21
+ | A specific item's value | `collection.get()` then access the item |
22
+
23
+ ```typescript
24
+ // Wrong — effect does not re-run when keys are added or removed
25
+ createEffect(() => {
26
+ const item = collection.byKey('id-123')
27
+ render(item)
28
+ })
29
+
30
+ // Correct — reading keys() creates a dependency on structural changes
31
+ createEffect(() => {
32
+ const keys = collection.keys() // tracks structure
33
+ const item = collection.byKey('id-123') // safe after establishing the edge
34
+ render(item)
35
+ })
36
+ ```
37
+ </direct_lookups_do_not_track>
38
+
39
+ <conditional_reads_delay_watched>
40
+ **Conditional signal reads delay `watched` activation.** The `watched` callback on a State
41
+ or Sensor fires when the first downstream effect subscribes. If a signal is only read inside
42
+ a branch that hasn't executed yet, `watched` does not fire until that branch runs.
43
+
44
+ Read all signals you care about eagerly — before any conditional logic — to ensure `watched`
45
+ fires on the first effect run:
46
+
47
+ ```typescript
48
+ // Bad — `derived` is only read after `task` resolves to `ok`
49
+ // `derived.watched` does not fire until the task has a value
50
+ createEffect(() => {
51
+ match([task], {
52
+ ok: ([result]) => render(derived.get(), result),
53
+ nil: () => showSpinner(),
54
+ })
55
+ })
56
+
57
+ // Good — both signals are read on every run, regardless of task state
58
+ // Both `watched` callbacks fire immediately when the effect is created
59
+ createEffect(() => {
60
+ match([task, derived], {
61
+ ok: ([result, value]) => render(value, result),
62
+ nil: () => showSpinner(),
63
+ })
64
+ })
65
+ ```
66
+
67
+ This also applies to plain `if` / ternary / `&&` patterns — any signal read gated behind a
68
+ condition may not establish its dependency edge until the condition is true.
69
+ </conditional_reads_delay_watched>
70
+
71
+ <equals_suppresses_subtrees>
72
+ **`equals` suppresses entire downstream subgraphs, not just the node it is set on.** When a
73
+ Memo or State recomputes to a value considered equal to the previous one, all downstream
74
+ nodes skip recomputation entirely without running their callbacks.
75
+
76
+ This is a powerful optimisation, but it has a non-obvious consequence: a custom `equals` on
77
+ an intermediate Memo can silently prevent large parts of the graph from updating, even if
78
+ upstream sources changed.
79
+
80
+ ```typescript
81
+ const source = createState({ x: 1, y: 2 })
82
+
83
+ // This memo compares by x only
84
+ const xOnly = createMemo(
85
+ () => source.get().x,
86
+ { equals: (a, b) => a === b }
87
+ )
88
+
89
+ // This effect depends on xOnly.
90
+ // It will NOT re-run if source changes but x stays the same,
91
+ // even if y changed dramatically.
92
+ createEffect(() => {
93
+ console.log('x is', xOnly.get())
94
+ })
95
+ ```
96
+
97
+ When debugging "why did my effect not re-run", check for custom `equals` on intermediate
98
+ memos in the dependency chain.
99
+ </equals_suppresses_subtrees>
100
+
101
+ <watched_stable_through_mutations>
102
+ **`watched` stays active through structural mutations.** The `watched` callback on a List or
103
+ Collection source is called once when the first downstream effect subscribes, and `unwatched`
104
+ is called when the last downstream effect unsubscribes. Structural mutations (adding items,
105
+ removing items, updating values) do not call `unwatched` then `watched` again — the callback
106
+ remains active for the lifetime of the subscription.
107
+
108
+ ```typescript
109
+ const list = createList(
110
+ () => startPolling(), // watched: called once when first effect subscribes
111
+ () => stopPolling(), // unwatched: called once when last effect unsubscribes
112
+ )
113
+
114
+ // These mutations do NOT restart the watched/unwatched cycle.
115
+ // The data source stays open as long as at least one effect is subscribed.
116
+ list.push({ id: '1', name: 'Item 1' }) // watched is NOT called again
117
+ list.delete('1') // watched is NOT called again
118
+ ```
119
+ </watched_stable_through_mutations>
120
+
121
+ <task_abort_on_dependency_change>
122
+ **A Task's `AbortSignal` is aborted when dependencies change before the async operation
123
+ completes.** If a Task's sources update while the previous `Promise` is still pending, a new
124
+ run is scheduled and the previous `AbortController` is aborted. Not forwarding the signal to
125
+ cancellable async operations will cause stale results to overwrite fresh ones.
126
+
127
+ ```typescript
128
+ // Wrong — fetch is not cancellable; a stale response may arrive after a newer one
129
+ const results = createTask(async () => {
130
+ return fetch(`/api/search?q=${query.get()}`).then(r => r.json())
131
+ })
132
+
133
+ // Correct — abort signal forwarded; stale in-flight requests are cancelled
134
+ const results = createTask(async (prev, signal) => {
135
+ return fetch(`/api/search?q=${query.get()}`, { signal }).then(r => r.json())
136
+ })
137
+ ```
138
+ </task_abort_on_dependency_change>
139
+
140
+ <sensor_unset_before_first_value>
141
+ **Reading a Sensor or Task before it has produced a value throws `UnsetSignalValueError`.**
142
+ Unlike State, these signals have no initial value — they are explicitly "unset" until the
143
+ first value arrives.
144
+
145
+ Guard against this with `match`, which provides a `nil` branch for the unset case:
146
+
147
+ ```typescript
148
+ const tick = createSensor<number>(set => {
149
+ const id = setInterval(() => set(Date.now()), 1000)
150
+ return () => clearInterval(id)
151
+ })
152
+
153
+ // Wrong — throws UnsetSignalValueError on first run, before the interval fires
154
+ createEffect(() => {
155
+ console.log(tick.get())
156
+ })
157
+
158
+ // Correct — match handles the nil (unset) case explicitly
159
+ createEffect(() => {
160
+ match([tick], {
161
+ ok: ([timestamp]) => console.log('tick:', timestamp),
162
+ nil: () => console.log('waiting for first tick…'),
163
+ })
164
+ })
165
+ ```
166
+ </sensor_unset_before_first_value>
167
+
168
+ <scope_cleanup_is_synchronous>
169
+ **Scope and Effect cleanup runs synchronously when the returned `Cleanup` function is
170
+ called.** It does not wait for the current flush to complete. Calling cleanup during a batch
171
+ (e.g. inside a `batch` callback) is safe but will immediately dispose the owner and all its
172
+ children.
173
+ </scope_cleanup_is_synchronous>
@@ -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>