@mrciphersmith/keryx 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/cli.js +13362 -7078
- package/dist/core.js +11706 -11330
- package/package.json +1 -1
- package/src/gdskills/bundled/agents/go-code-auditor.md +1 -1
- package/src/gdskills/bundled/agents/python-code-auditor.md +1 -1
- package/src/gdskills/bundled/install-manifest.json +271 -4
- package/src/gdskills/bundled/rules/core/model-selection.mdc +51 -0
- package/src/gdskills/bundled/skills/review/review-jev-comments/SKILL.md +184 -0
- package/src/gdskills/bundled/skills/review/review-jev-docs/SKILL.md +189 -0
- package/src/gdskills/bundled/skills/review/review-jev-risk/SKILL.md +190 -0
- package/src/gdskills/bundled/skills/review/review-jev-rules/SKILL.md +267 -0
- package/src/gdskills/bundled/skills/review/review-jev-scenarios/SKILL.md +187 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +39 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +1 -1
- package/src/gdskills/bundled/stacks/angular/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/angular/governance/eval.json +1751 -0
- package/src/gdskills/bundled/stacks/angular/governance/scout.json +32 -0
- package/src/gdskills/bundled/stacks/angular/pack.json +55 -0
- package/src/gdskills/bundled/stacks/angular/rules/coding-style.mdc +82 -0
- package/src/gdskills/bundled/stacks/angular/rules/patterns.mdc +84 -0
- package/src/gdskills/bundled/stacks/angular/rules/security.mdc +70 -0
- package/src/gdskills/bundled/stacks/angular/rules/testing.mdc +73 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/SKILL.md +127 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/SKILL.md +98 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/SKILL.md +112 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-testing/SKILL.md +102 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-testing/evals.json +71 -0
- package/src/gdskills/bundled/stacks/mobx/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/mobx/governance/eval.json +904 -0
- package/src/gdskills/bundled/stacks/mobx/governance/scout.json +18 -0
- package/src/gdskills/bundled/stacks/mobx/pack.json +28 -0
- package/src/gdskills/bundled/stacks/mobx/rules/coding-style.mdc +91 -0
- package/src/gdskills/bundled/stacks/mobx/rules/patterns.mdc +122 -0
- package/src/gdskills/bundled/stacks/mobx/rules/security.mdc +56 -0
- package/src/gdskills/bundled/stacks/mobx/rules/testing.mdc +63 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/SKILL.md +149 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/nestjs/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/nestjs/governance/eval.json +1308 -0
- package/src/gdskills/bundled/stacks/nestjs/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/nestjs/pack.json +53 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/coding-style.mdc +70 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/patterns.mdc +83 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/security.mdc +73 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/testing.mdc +69 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/SKILL.md +157 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/evals.json +70 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/SKILL.md +129 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/evals.json +71 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/evals.json +69 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/eval.json +2413 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/scout.json +42 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/pack.json +42 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/patterns.mdc +88 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/security.mdc +72 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/testing.mdc +64 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/evals.json +75 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/SKILL.md +118 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/evals.json +78 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/SKILL.md +116 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/evals.json +75 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/evals.json +76 -0
- package/src/gdskills/bundled/stacks/vue/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/vue/governance/eval.json +2215 -0
- package/src/gdskills/bundled/stacks/vue/governance/scout.json +42 -0
- package/src/gdskills/bundled/stacks/vue/pack.json +42 -0
- package/src/gdskills/bundled/stacks/vue/rules/coding-style.mdc +73 -0
- package/src/gdskills/bundled/stacks/vue/rules/patterns.mdc +84 -0
- package/src/gdskills/bundled/stacks/vue/rules/security.mdc +60 -0
- package/src/gdskills/bundled/stacks/vue/rules/testing.mdc +69 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/SKILL.md +137 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/SKILL.md +120 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/evals.json +71 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/SKILL.md +122 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-testing/SKILL.md +115 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-testing/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/evals.json +71 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Use when writing or extending a MobX store: adding observable state, actions, computed getters, or reactions (autorun/reaction/when), wiring a store into React via observer and a context hook, or fixing a component that stops re-rendering after a store change. Also covers plain-language asks for the same work: keeping a piece of MobX store state automatically in sync wherever it's read, or making a store run something automatically when a value changes and stop when the store is no longer needed. Applies the makeObservable/action/runInAction/observer shape and the store's dispose lifecycle. Not for reviewing an already-written store's structure (use code-mobx-store-review) and not for plain React state/props work with no MobX involved (use react-implementation).",
|
|
4
|
+
"decision": "fork",
|
|
5
|
+
"topMatch": "mobx/mobx-observable-testing",
|
|
6
|
+
"recordedAt": "2026-09-25T06:28:15.470Z",
|
|
7
|
+
"skillName": "mobx-store-implementation",
|
|
8
|
+
"justification": "Top match remains this pack's own sibling mobx-observable-testing -- same domain vocabulary (store/action/computed/observable), different category (implement vs test), not a substitute: that skill only covers writing/fixing tests, never authoring store code. Description was widened (flow 318 follow-up) to also cover plain-language symptom/goal phrasings of the same implement work, not to broaden scope -- no new candidate skill/pack became a closer match after the update; review-frontend, nestjs-implementation, code-mobx-store-review, and angular-implementation remain the next nearest and are all a different stack/activity or review-only."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "Use when writing or fixing a test for a MobX store: asserting on an async action's post-await state, waiting for a reaction/autorun to fire, testing that dispose() actually stops a store's reactions, or deciding how enforceActions should behave in the test setup. Distinct from rendering-focused React component testing and generic Node/TypeScript test-runner setup. Not for writing the store or action code itself (use mobx-store-implementation) and not for React Testing Library render/interaction patterns with no MobX involved (use react-testing).",
|
|
12
|
+
"decision": "fork",
|
|
13
|
+
"topMatch": "mobx/mobx-store-implementation",
|
|
14
|
+
"recordedAt": "2026-09-25T06:24:35.475Z",
|
|
15
|
+
"skillName": "mobx-observable-testing",
|
|
16
|
+
"justification": "Top match (0.38) is this pack's own sibling mobx-store-implementation -- same domain vocabulary (store/action/reaction/dispose), different category (test vs implement), not a substitute: that skill authors store/action code, this one asserts on the resulting async/reaction behavior in tests. Next matches (vue-testing 0.25, nodejs-implementation 0.22, angular-code-review 0.21, review-frontend 0.21) are all a different stack or activity entirely -- generic test-runner/component-render skills with no MobX-specific reactivity (runInAction seeding, when()-based waits, dispose/reaction cleanup assertions) this skill covers."
|
|
17
|
+
}
|
|
18
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "mobx",
|
|
3
|
+
"family": "framework",
|
|
4
|
+
"extends": "react",
|
|
5
|
+
"modules": [
|
|
6
|
+
"mobx-rules",
|
|
7
|
+
"mobx-skills"
|
|
8
|
+
],
|
|
9
|
+
"detectionMarkers": [
|
|
10
|
+
"mobx"
|
|
11
|
+
],
|
|
12
|
+
"provenance": {
|
|
13
|
+
"origin": "authored",
|
|
14
|
+
"sourceRef": "flow 318, Wave 4 batch 2"
|
|
15
|
+
},
|
|
16
|
+
"stability": "experimental",
|
|
17
|
+
"skills": {
|
|
18
|
+
"implement": [
|
|
19
|
+
"mobx-store-implementation"
|
|
20
|
+
],
|
|
21
|
+
"test": [
|
|
22
|
+
"mobx-observable-testing"
|
|
23
|
+
],
|
|
24
|
+
"review": [],
|
|
25
|
+
"build-fix": [],
|
|
26
|
+
"migrate": []
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.ts", "**/*.tsx"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MobX coding style
|
|
9
|
+
|
|
10
|
+
Narrows the React/TypeScript common rules to MobX's own idiom for stores,
|
|
11
|
+
observables, and observer components. Everything not MobX-specific still
|
|
12
|
+
comes from the common and React rules this file `extends`.
|
|
13
|
+
|
|
14
|
+
## Store class shape
|
|
15
|
+
|
|
16
|
+
- Call `makeObservable(this)` (with an explicit annotations map, or
|
|
17
|
+
`makeAutoObservable(this)` for a store with no inheritance and no need to
|
|
18
|
+
keep some fields non-observable) once, in the constructor, before any
|
|
19
|
+
other constructor logic reads `this`.
|
|
20
|
+
- Prefer explicit `@observable`/`@computed`/`@action`/`@action.bound`
|
|
21
|
+
decorators with `makeObservable` over `makeAutoObservable` for any store
|
|
22
|
+
a component subclasses, that mixes injected dependencies with state, or
|
|
23
|
+
that needs a field to stay non-observable — `makeAutoObservable` makes
|
|
24
|
+
every own enumerable property observable and every method an action,
|
|
25
|
+
which is the wrong default once the store has structure to protect.
|
|
26
|
+
- Name store classes `XyzStore` and files `xyz.store.ts`/`XyzStore.ts` to
|
|
27
|
+
match the project's existing convention; keep one store per file.
|
|
28
|
+
|
|
29
|
+
## Actions
|
|
30
|
+
|
|
31
|
+
- Every method that mutates observable state must be an `action`
|
|
32
|
+
(`@action` or `@action.bound`). A public method invoked from a component
|
|
33
|
+
event handler must be `@action.bound` so it can be passed as a callback
|
|
34
|
+
reference without losing `this`.
|
|
35
|
+
- After an `await` inside an async action, re-enter MobX with
|
|
36
|
+
`runInAction(() => { ... })` before mutating state — the code after the
|
|
37
|
+
`await` runs outside the original action's transaction.
|
|
38
|
+
- Do not call a store method that mutates state directly from a component
|
|
39
|
+
render body; only from an event handler, effect, or another action.
|
|
40
|
+
|
|
41
|
+
## Computed values
|
|
42
|
+
|
|
43
|
+
- Any value derived purely from other observables belongs in a
|
|
44
|
+
`@computed get` accessor, not in a component-side calculation repeated
|
|
45
|
+
on every render or a store method that recomputes and returns a fresh
|
|
46
|
+
value each call. A `@computed` is cached and only re-evaluates when one
|
|
47
|
+
of its own observable dependencies changes.
|
|
48
|
+
- Keep a `@computed` getter free of side effects; it must be safe to
|
|
49
|
+
evaluate speculatively (MobX may read a computed's value to decide
|
|
50
|
+
whether it changed).
|
|
51
|
+
|
|
52
|
+
## Observable collections and value types
|
|
53
|
+
|
|
54
|
+
- Use `@observable.ref` for a value the store treats as an opaque
|
|
55
|
+
reference (a large externally-owned object, a DOM/editor instance) it
|
|
56
|
+
does not want deeply proxied, `@observable.shallow` for a container
|
|
57
|
+
whose items should stay plain, and `@observable.struct` when a reaction
|
|
58
|
+
should only fire on a structural (deep-equal) change, not fine-grained.
|
|
59
|
+
Reserve the default deep `@observable` for state the store actually
|
|
60
|
+
wants MobX to track recursively.
|
|
61
|
+
- When you need MobX's own collection API (`.replace()`, `.remove()`,
|
|
62
|
+
`.merge()`, `.has()`) on an array or map field, construct it with
|
|
63
|
+
`observable([...])` / `observable(new Map())` and type it as
|
|
64
|
+
`IObservableArray<T>` / `ObservableMap<K, V>`; a plain array/object
|
|
65
|
+
literal assigned to `@observable` is still deeply observed but does not
|
|
66
|
+
expose those methods.
|
|
67
|
+
|
|
68
|
+
## Components
|
|
69
|
+
|
|
70
|
+
- Wrap any function component that reads observable state in `observer`
|
|
71
|
+
from `mobx-react-lite`. A component that reads a store field but is not
|
|
72
|
+
wrapped in `observer` will render once and silently never update again
|
|
73
|
+
when that field changes.
|
|
74
|
+
- Read store instances through a typed React context + hook pair
|
|
75
|
+
(`useXyzStore()`), not through a module-level singleton import, so
|
|
76
|
+
components stay testable and providers can swap the instance.
|
|
77
|
+
- Do not destructure observable fields off a store at the top of a
|
|
78
|
+
component body if that breaks fine-grained tracking for a nested
|
|
79
|
+
`<Observer>` boundary further down — read the field where it is
|
|
80
|
+
actually rendered instead.
|
|
81
|
+
|
|
82
|
+
## Errors and imports
|
|
83
|
+
|
|
84
|
+
- Import MobX identifiers (`observable`, `action`, `computed`, `reaction`,
|
|
85
|
+
`runInAction`, `makeObservable`) from `"mobx"`, and `observer`,
|
|
86
|
+
`useLocalObservable` from `"mobx-react-lite"` — do not import from the
|
|
87
|
+
deprecated combined `mobx-react` package in new code unless the project
|
|
88
|
+
has not migrated off it yet.
|
|
89
|
+
- Catch and store errors from an async action's `try/catch` as
|
|
90
|
+
`err: unknown`, narrowing before assigning to an observable `error`
|
|
91
|
+
field (see `rules/patterns.mdc` for the full async-action shape).
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.ts", "**/*.tsx"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MobX patterns
|
|
9
|
+
|
|
10
|
+
Idiomatic MobX design patterns and their anti-patterns, narrowing the
|
|
11
|
+
common architecture rules to how state, actions, and reactions should be
|
|
12
|
+
composed in a MobX + React codebase.
|
|
13
|
+
|
|
14
|
+
## The async action shape
|
|
15
|
+
|
|
16
|
+
The standard shape for a store method that calls an API and updates
|
|
17
|
+
loading/error/data state:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
private performFetch = async () => {
|
|
21
|
+
try {
|
|
22
|
+
runInAction(() => {
|
|
23
|
+
this.loading = true;
|
|
24
|
+
this.error = null;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const data = await this.service.getAll();
|
|
28
|
+
|
|
29
|
+
runInAction(() => {
|
|
30
|
+
if (this.disposed) return;
|
|
31
|
+
this.items = data;
|
|
32
|
+
});
|
|
33
|
+
} catch (err: unknown) {
|
|
34
|
+
runInAction(() => {
|
|
35
|
+
if (this.disposed) return;
|
|
36
|
+
this.error = err instanceof Error ? err.message : "Fetch failed";
|
|
37
|
+
});
|
|
38
|
+
} finally {
|
|
39
|
+
runInAction(() => {
|
|
40
|
+
if (this.disposed) return;
|
|
41
|
+
this.loading = false;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Anti-pattern: mutating `this.loading`/`this.items` directly after the
|
|
48
|
+
`await` with no `runInAction` — this throws under `enforceActions: "observed"`
|
|
49
|
+
and silently misses the reaction under looser settings, either way
|
|
50
|
+
producing a UI that does not update.
|
|
51
|
+
|
|
52
|
+
## `flow` as an alternative to `runInAction`
|
|
53
|
+
|
|
54
|
+
For a store with many async actions, `flow` (a generator-based action)
|
|
55
|
+
avoids repeating `runInAction` around every post-`await` mutation, since
|
|
56
|
+
the whole generator body already runs inside one action:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { flow } from "mobx";
|
|
60
|
+
|
|
61
|
+
fetchItems = flow(function* (this: ItemStore) {
|
|
62
|
+
this.loading = true;
|
|
63
|
+
try {
|
|
64
|
+
this.items = yield this.service.getAll();
|
|
65
|
+
} finally {
|
|
66
|
+
this.loading = false;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use `flow` when a store's async methods are mostly sequential awaits with
|
|
72
|
+
state writes in between; keep plain `async`/`runInAction` when a method
|
|
73
|
+
needs to interleave with non-MobX async code (`Promise.all`, cancellation
|
|
74
|
+
tokens) where the generator form adds friction.
|
|
75
|
+
|
|
76
|
+
## Reaction ownership and disposal
|
|
77
|
+
|
|
78
|
+
- `reaction`/`autorun`/`when` created inside a store belong to that
|
|
79
|
+
store's lifecycle: push the disposer into a `disposers: IReactionDisposer[]`
|
|
80
|
+
array in the constructor (or an `init()` called by the parent) and call
|
|
81
|
+
every disposer in `dispose()`.
|
|
82
|
+
- Prefer `reaction(() => selector, handler)` over `autorun` when the
|
|
83
|
+
handler should only run in response to a specific observable changing,
|
|
84
|
+
not on every observable it happens to read — `autorun` re-subscribes to
|
|
85
|
+
everything it touches on each run, which can widen its dependency set
|
|
86
|
+
unintentionally.
|
|
87
|
+
- A component should not create a raw `autorun`/`reaction` in its render
|
|
88
|
+
body; either derive the value with `@computed` in the store and let
|
|
89
|
+
`observer` handle re-rendering, or start the reaction in a `useEffect`
|
|
90
|
+
and dispose it in the cleanup function.
|
|
91
|
+
|
|
92
|
+
## Bidirectional store sync needs an equality guard
|
|
93
|
+
|
|
94
|
+
When Store A writes to Store B and Store B writes back to Store A, guard
|
|
95
|
+
the reverse direction with `if (newValue !== currentValue)` before
|
|
96
|
+
propagating, or the two stores bounce a change back and forth forever.
|
|
97
|
+
See `rules/coding-style.mdc` for where that callback belongs (private,
|
|
98
|
+
not a public UI action).
|
|
99
|
+
|
|
100
|
+
## View↔Store boundary
|
|
101
|
+
|
|
102
|
+
- A component reads store state and calls store actions; it does not
|
|
103
|
+
hold a parallel copy of store state in `useState` "for convenience" —
|
|
104
|
+
that copy silently desyncs from the store the first time an action
|
|
105
|
+
outside that component's tree mutates the same field.
|
|
106
|
+
- Data loading triggered by mounting belongs in the owning store's
|
|
107
|
+
`init()`/`onMount()`, called by the *parent* store or the top-level
|
|
108
|
+
provider — not scattered across every consuming component's
|
|
109
|
+
`useEffect`, which makes it impossible to reason about how many times a
|
|
110
|
+
fetch fires as the tree re-renders.
|
|
111
|
+
- A store must not import a React component or hook; it may accept
|
|
112
|
+
plain callback props for the rare case a parent needs to react to a
|
|
113
|
+
child store's internal event.
|
|
114
|
+
|
|
115
|
+
## Anti-pattern: computed-as-action
|
|
116
|
+
|
|
117
|
+
A `@computed` getter must never mutate state — a getter with a
|
|
118
|
+
side-effecting write inside it runs outside an explicit action
|
|
119
|
+
transaction and MobX will throw ("computed value is allowed to be
|
|
120
|
+
mutated only through action") the moment `enforceActions` is on. If a
|
|
121
|
+
"derived" value also needs to cache an expensive side effect, that is an
|
|
122
|
+
action-triggered `reaction`, not a computed.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.ts", "**/*.tsx"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MobX security
|
|
9
|
+
|
|
10
|
+
Stack-specific risks narrowing the common OWASP-style rules to MobX state
|
|
11
|
+
management. Most web security concerns (XSS, auth, injection) are
|
|
12
|
+
React/general-web concerns already covered by the common and React rules
|
|
13
|
+
this file `extends`; this file covers the risks that are specific to
|
|
14
|
+
putting sensitive data in observable state.
|
|
15
|
+
|
|
16
|
+
## Do not put secrets in long-lived observable state
|
|
17
|
+
|
|
18
|
+
- A token, API key, or credential held in an `@observable` field on a
|
|
19
|
+
store that survives the session (not cleared on logout, persisted via
|
|
20
|
+
`autorun`/`reaction` to `localStorage`) is exposed to any code with
|
|
21
|
+
access to that store instance and to any DevTools-based MobX inspector
|
|
22
|
+
attached to the page. Keep secrets out of observable state; pass them
|
|
23
|
+
through a request layer that does not put them on a reactive object a
|
|
24
|
+
component tree (and any debugging tool observing it) can read.
|
|
25
|
+
- If a `reaction`/`autorun` persists store state to `localStorage`/
|
|
26
|
+
`sessionStorage` (a common MobX pattern for "remember this UI state"),
|
|
27
|
+
explicitly exclude any field carrying a token, PII, or payment data from
|
|
28
|
+
what gets serialized — persist a derived, scrubbed snapshot, not the
|
|
29
|
+
whole store.
|
|
30
|
+
|
|
31
|
+
## Untrusted data into observable state is still untrusted
|
|
32
|
+
|
|
33
|
+
- Assigning server or user-supplied HTML/markdown into an observable
|
|
34
|
+
field does not sanitize it. If that field is later rendered with
|
|
35
|
+
`dangerouslySetInnerHTML` in an `observer` component, the same XSS risk
|
|
36
|
+
applies as any other React render path — sanitize before it reaches the
|
|
37
|
+
observable, not by trusting MobX's reactivity to be a security boundary
|
|
38
|
+
(see `rules/security.mdc` in the `react` pack this one extends).
|
|
39
|
+
|
|
40
|
+
## Reaction-driven side effects and SSRF/open-redirect risk
|
|
41
|
+
|
|
42
|
+
- A `reaction`/`autorun` that fires a `fetch`/navigation based on an
|
|
43
|
+
observable URL or hostname field must validate that value the same way
|
|
44
|
+
a request-time input would be validated — an attacker who can influence
|
|
45
|
+
the observable (e.g. via a query param synced into the store) can
|
|
46
|
+
otherwise trigger a request to an attacker-chosen host purely through
|
|
47
|
+
state changes, with no obvious call site to audit.
|
|
48
|
+
|
|
49
|
+
## Disposal prevents stale-store data leaks across users/sessions
|
|
50
|
+
|
|
51
|
+
- A store not disposed on logout/route-change can keep the previous
|
|
52
|
+
user's data in memory and observable to any component that still holds
|
|
53
|
+
a reference (e.g. a lingering `reaction` on a background tab). Call
|
|
54
|
+
`dispose()` on session/user change for any store holding
|
|
55
|
+
user-scoped data, not just to free memory — to stop rendering or
|
|
56
|
+
syncing stale, now-unauthorized data.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.ts", "**/*.tsx"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MobX testing
|
|
9
|
+
|
|
10
|
+
Narrows the common testing rules to what is specific about testing MobX
|
|
11
|
+
stores, actions, and reactions — test layout, runner, and framework
|
|
12
|
+
concerns for React components themselves still come from the `react`
|
|
13
|
+
pack's own `rules/testing.mdc`.
|
|
14
|
+
|
|
15
|
+
## Store tests are plain unit tests
|
|
16
|
+
|
|
17
|
+
- A store with no React dependency (no `observer`, no hook) is tested by
|
|
18
|
+
constructing it directly and calling its actions/reading its computeds
|
|
19
|
+
— no React Testing Library render is needed. Co-locate the test next to
|
|
20
|
+
the store (`xyz.store.test.ts` beside `xyz.store.ts`), matching the
|
|
21
|
+
project's existing test-file convention.
|
|
22
|
+
- Mock the store's injected dependencies (an API service passed into the
|
|
23
|
+
constructor), not MobX itself — MobX's reactivity is the thing under
|
|
24
|
+
test, not something to stub out.
|
|
25
|
+
|
|
26
|
+
## Awaiting async actions and reactions
|
|
27
|
+
|
|
28
|
+
- After calling an async action in a test, `await` the action's own
|
|
29
|
+
returned promise (for a plain `async` method) or `await` a `when(...)`
|
|
30
|
+
predicate on the resulting state (`await when(() => !store.loading)`)
|
|
31
|
+
before asserting — do not assert immediately after the call and rely on
|
|
32
|
+
a fixed `setTimeout`/sleep, which is both slower and flaky.
|
|
33
|
+
- When asserting on a value only a `reaction`/`autorun` sets, wait for
|
|
34
|
+
that reaction to have run (`await when(() => condition)`, or `await
|
|
35
|
+
Promise.resolve()` for microtask-only sync reactions) rather than
|
|
36
|
+
asserting synchronously right after the triggering mutation.
|
|
37
|
+
|
|
38
|
+
## `enforceActions` in tests
|
|
39
|
+
|
|
40
|
+
- Keep `enforceActions` at the project's real (usually `"observed"` or
|
|
41
|
+
`"always"`) setting in tests rather than relaxing it to `"never"` for
|
|
42
|
+
convenience — a test that mutates observable state outside an action
|
|
43
|
+
should fail the same way production code would, since that failure is
|
|
44
|
+
exactly the bug class these tests exist to catch.
|
|
45
|
+
- If a test genuinely needs to seed store state directly (bypassing
|
|
46
|
+
actions, e.g. to set up a precondition), wrap that seeding assignment
|
|
47
|
+
in `runInAction` rather than disabling `enforceActions` for the whole
|
|
48
|
+
suite.
|
|
49
|
+
|
|
50
|
+
## Testing disposal and reaction cleanup
|
|
51
|
+
|
|
52
|
+
- Assert that `dispose()` actually stops a store's reactions: trigger a
|
|
53
|
+
mutation after `dispose()` and confirm the disposed reaction's effect
|
|
54
|
+
did not run again — a leaked, undisposed `autorun`/`reaction` is a real
|
|
55
|
+
bug that a naive "does dispose() throw" test will not catch.
|
|
56
|
+
|
|
57
|
+
## Component tests still need `observer`-aware setup
|
|
58
|
+
|
|
59
|
+
- A test rendering an `observer` component must trigger the store
|
|
60
|
+
mutation the same way production code would (through the store's own
|
|
61
|
+
action) and then assert on the rendered output after that action
|
|
62
|
+
resolves — do not directly reassign an observable field the component
|
|
63
|
+
reads and expect the same ordering guarantees an action provides.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobx-observable-testing
|
|
3
|
+
description: "Use when writing or fixing a test for a MobX store: asserting on an async action's post-await state, waiting for a reaction/autorun to fire, testing that dispose() actually stops a store's reactions, or deciding how enforceActions should behave in the test setup. Distinct from rendering-focused React component testing and generic Node/TypeScript test-runner setup. Not for writing the store or action code itself (use mobx-store-implementation) and not for React Testing Library render/interaction patterns with no MobX involved (use react-testing)."
|
|
4
|
+
triggers:
|
|
5
|
+
- "write a test for this MobX store's async fetch action"
|
|
6
|
+
- "test that this store's reaction actually fires"
|
|
7
|
+
- "my store test asserts before the async action finishes"
|
|
8
|
+
- "test that dispose() stops this store's autorun"
|
|
9
|
+
- "how should enforceActions behave in tests"
|
|
10
|
+
- "test this computed getter updates when the observable changes"
|
|
11
|
+
- "flaky MobX store test that sometimes reads stale state"
|
|
12
|
+
metadata:
|
|
13
|
+
origin: authored
|
|
14
|
+
category: test
|
|
15
|
+
version: "1.0.0"
|
|
16
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
17
|
+
license: "MIT"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# MobX observable testing
|
|
21
|
+
|
|
22
|
+
Write or fix a test that exercises a MobX store's actions, computed
|
|
23
|
+
values, or reactions directly — not through a component render. See
|
|
24
|
+
`rules/testing.mdc` for the full set of conventions this skill applies,
|
|
25
|
+
and `rules/patterns.mdc` for the async-action shape being tested.
|
|
26
|
+
|
|
27
|
+
## Scope
|
|
28
|
+
|
|
29
|
+
This skill is for testing MobX's own reactivity: does an action correctly
|
|
30
|
+
update state, does a computed recompute, does a reaction fire and get
|
|
31
|
+
disposed. It is not for writing the store/action code under test (use
|
|
32
|
+
`mobx-store-implementation`) and not for React Testing Library
|
|
33
|
+
render/interaction mechanics with no MobX-specific concern (use the
|
|
34
|
+
`react` pack's testing skill) — a test that only checks a button click
|
|
35
|
+
calls a prop handler has nothing MobX-specific in it and does not need
|
|
36
|
+
this skill.
|
|
37
|
+
|
|
38
|
+
## Workflow
|
|
39
|
+
|
|
40
|
+
### Step 1: Identify what kind of MobX behavior is under test
|
|
41
|
+
|
|
42
|
+
- A synchronous action's effect on observable state — assert immediately
|
|
43
|
+
after calling it.
|
|
44
|
+
- An async action's effect on state after an `await` — the assertion has
|
|
45
|
+
to wait for the action's own promise (or a predicate) to resolve, not
|
|
46
|
+
run synchronously.
|
|
47
|
+
- A `@computed` getter — assert it changes when its own observable
|
|
48
|
+
dependency changes, and does not change when an unrelated field does.
|
|
49
|
+
- A `reaction`/`autorun`/`when` — assert its effect actually runs after
|
|
50
|
+
the triggering mutation, and stops running after `dispose()`.
|
|
51
|
+
|
|
52
|
+
### Step 2: Construct the store with mocked dependencies
|
|
53
|
+
|
|
54
|
+
Instantiate the store directly with mocked injected dependencies (an API
|
|
55
|
+
service stub), not a full component tree. Keep `enforceActions` at the
|
|
56
|
+
project's real setting (see `rules/testing.mdc`) — do not loosen it in
|
|
57
|
+
test setup just to make an assertion easier to write.
|
|
58
|
+
|
|
59
|
+
### Step 3: Write the assertion with the right wait
|
|
60
|
+
|
|
61
|
+
- Sync action: call it, assert immediately.
|
|
62
|
+
- Async action: `await` the action's returned promise directly if it's a
|
|
63
|
+
plain `async` method; otherwise `await when(() => <the condition the
|
|
64
|
+
action should reach>)` before asserting.
|
|
65
|
+
- Reaction/autorun: trigger the mutation that should fire it, then
|
|
66
|
+
`await when(() => <the reaction's expected effect>)` (or `await
|
|
67
|
+
Promise.resolve()` for a synchronous microtask reaction) before
|
|
68
|
+
asserting the effect ran.
|
|
69
|
+
- Never assert immediately after a mutation that a reaction depends on
|
|
70
|
+
without first waiting for that reaction to run, and never paper over a
|
|
71
|
+
flaky wait with a fixed `setTimeout`/sleep — it is both slower and
|
|
72
|
+
still not deterministic under load.
|
|
73
|
+
|
|
74
|
+
### Step 4: Test disposal
|
|
75
|
+
|
|
76
|
+
For a store owning `autorun`/`reaction`/`when`, add a test that calls
|
|
77
|
+
`dispose()`, then triggers the mutation that would normally fire the
|
|
78
|
+
reaction, and asserts the reaction's effect did NOT run again — a test
|
|
79
|
+
that only checks `dispose()` doesn't throw misses the actual leak.
|
|
80
|
+
|
|
81
|
+
### Step 5: Verify
|
|
82
|
+
|
|
83
|
+
Run the project's test command and confirm the new/changed tests are
|
|
84
|
+
deterministic (run them a few times in a row locally if they involve
|
|
85
|
+
timing) and pass without a suppressed assertion.
|
|
86
|
+
|
|
87
|
+
## Rules
|
|
88
|
+
|
|
89
|
+
- Follow `rules/testing.mdc` for store test layout, mocking, and
|
|
90
|
+
`enforceActions` behavior.
|
|
91
|
+
- ALWAYS wait for an async action's own completion (its promise, or a
|
|
92
|
+
`when(...)` predicate) before asserting on state it sets — never assert
|
|
93
|
+
synchronously right after calling it.
|
|
94
|
+
- ALWAYS keep `enforceActions` at the project's real setting; seed
|
|
95
|
+
precondition state through `runInAction`, not by disabling it.
|
|
96
|
+
- NEVER assert on a reaction's effect without first waiting for the
|
|
97
|
+
reaction to actually run.
|
|
98
|
+
- NEVER skip or delete a store test to make a suite pass; fix the source
|
|
99
|
+
if behavior regressed, or fix the test if its expectation was stale —
|
|
100
|
+
state which, and why.
|
|
101
|
+
|
|
102
|
+
## Red Flags
|
|
103
|
+
|
|
104
|
+
| Rationalization | Why it is wrong |
|
|
105
|
+
|---|---|
|
|
106
|
+
| "I'll just add a `setTimeout(resolve, 100)` before asserting, it's fast enough in CI" | A fixed sleep is both slower than necessary and still not deterministic under load; wait on the action's own promise or a `when(...)` predicate instead |
|
|
107
|
+
| "I'll set `enforceActions: 'never'` in the test file so I can mutate state directly to set up my fixture" | That hides the same bug class the real app would hit; seed fixture state through `runInAction` instead |
|
|
108
|
+
| "The dispose() test just checks it doesn't throw, that's enough" | A store can dispose without ever having wired up its reactions correctly; assert the reaction's effect actually stops firing after dispose |
|
|
109
|
+
| "This store test is flaky, I'll skip it for now" | Skipping hides a real timing bug in the store or the test's own wait strategy; find the missing `await`/`when` instead |
|
|
110
|
+
|
|
111
|
+
## Verification
|
|
112
|
+
|
|
113
|
+
Do not report a store test done until all of the following hold:
|
|
114
|
+
|
|
115
|
+
- The project's test command passes for the new/changed test.
|
|
116
|
+
- Every assertion on async-action or reaction state waits for that
|
|
117
|
+
action's promise or a `when(...)` predicate first — no bare
|
|
118
|
+
`setTimeout`/sleep.
|
|
119
|
+
- `enforceActions` was not loosened in the test file to make setup
|
|
120
|
+
easier.
|
|
121
|
+
- A store owning disposable reactions has a test proving `dispose()`
|
|
122
|
+
actually stops them, not just that it runs without throwing.
|
|
123
|
+
- Re-running the test locally 2-3 times in a row produces the same
|
|
124
|
+
result (no flake from a missing wait).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"I need a test for this MobX store's async method that fetches data from the API -- how do I make sure it actually waits for the result before I assert?",
|
|
5
|
+
"My store test asserts on this.items right after calling the async action and it's flaky",
|
|
6
|
+
"How do I test that this MobX store actually does something automatically when a value changes?",
|
|
7
|
+
"Write a test proving dispose() stops this store's reaction from running again",
|
|
8
|
+
"Should enforceActions be relaxed in my MobX store test setup?",
|
|
9
|
+
"How do I test that a derived value on this MobX store updates correctly when the thing it depends on changes?",
|
|
10
|
+
"This store test sometimes reads stale state before the reaction has run, fix the wait"
|
|
11
|
+
],
|
|
12
|
+
"negative": [
|
|
13
|
+
"Add a new @action.bound method to this store's constructor that fetches items from the API and wraps the loading/error update in runInAction",
|
|
14
|
+
"Write a React Testing Library test that clicks this button and checks the handler prop was called",
|
|
15
|
+
"Add a @computed get totalPrice getter to this store class that sums the items array",
|
|
16
|
+
"Write a Vitest test for this plain utility function that formats a currency string",
|
|
17
|
+
"Fix this tsc error: Property 'total' does not exist on type 'OrderDraft'",
|
|
18
|
+
"Review this MobX store for accessibility-modifier and method-ordering issues"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"scenarios": [
|
|
22
|
+
{
|
|
23
|
+
"id": "assert-after-async-action",
|
|
24
|
+
"prompt": "This test is flaky:\n\n```ts\nit(\"loads items\", () => {\n store.fetchItems();\n expect(store.items).toHaveLength(3);\n});\n```\n\n`fetchItems` is an async action. Why is this flaky and how do I fix it?",
|
|
25
|
+
"strictness": "high",
|
|
26
|
+
"expected_behavior": [
|
|
27
|
+
{
|
|
28
|
+
"grader": "judge",
|
|
29
|
+
"rubric": "A correct answer explains that the assertion runs before the async fetchItems action has finished updating state, and fixes it by awaiting the action's own returned promise (or a when(...) predicate on the resulting state) before asserting, showing the corrected test code.",
|
|
30
|
+
"pass_criteria": [
|
|
31
|
+
"States that the test asserts synchronously right after calling fetchItems, before the async action's await has resolved and updated store.items, which is why the assertion sometimes sees stale/empty state",
|
|
32
|
+
"Shows the corrected test awaiting the action's own promise (e.g. `await store.fetchItems();`) or a `when(...)` predicate on the resulting state before the expect call"
|
|
33
|
+
],
|
|
34
|
+
"fail_criteria": [
|
|
35
|
+
"Recommends adding a fixed setTimeout/sleep delay before the assertion instead of awaiting the action's own promise or a when(...) predicate -- mentioning that only to warn against it is not a failure",
|
|
36
|
+
"Leaves the assertion running synchronously right after the fetchItems() call with no await or wait predicate added at all"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"calibration": {
|
|
41
|
+
"known_right": "The test calls `store.fetchItems()` without awaiting it, then asserts immediately -- but fetchItems is async, so the assertion runs before the underlying await (the API call) has resolved and the action has had a chance to set store.items. That's why it's flaky: sometimes the promise resolves fast enough by coincidence, sometimes it doesn't. Fix it by awaiting the action itself:\n\n```ts\nit(\"loads items\", async () => {\n await store.fetchItems();\n expect(store.items).toHaveLength(3);\n});\n```\n\nIf fetchItems doesn't return its promise (e.g. it's fire-and-forget from a UI action), wait on a predicate instead: `await when(() => !store.loading);` before asserting. Either way, the test needs to wait for the actual async work to finish rather than assuming it already has.",
|
|
42
|
+
"known_wrong": "Flaky async tests like this usually just need a bit more time to settle -- add a small delay before the assertion: `store.fetchItems(); await new Promise(r => setTimeout(r, 200)); expect(store.items).toHaveLength(3);`. 200ms should be more than enough for the fetch to resolve in a test environment, and it's a quick fix that doesn't require changing fetchItems itself.",
|
|
43
|
+
"vague": "The test probably isn't waiting long enough for the async action to complete before checking the result, so you need to make sure it waits properly.",
|
|
44
|
+
"subtle_wrong": "I made the test function async and added `await Promise.resolve();` after calling fetchItems, since that lets one microtask cycle run before the assertion: `it(\"loads items\", async () => { store.fetchItems(); await Promise.resolve(); expect(store.items).toHaveLength(3); });`. That should cover most cases since a single microtask flush usually catches up with pending promise resolutions in this test environment."
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "dispose-test-effect",
|
|
49
|
+
"prompt": "I have a store with an autorun disposed in dispose(). My current test is:\n\n```ts\nit(\"disposes cleanly\", () => {\n store.dispose();\n});\n```\n\nDoes this actually prove the autorun stops running? If not, what should the test do instead?",
|
|
50
|
+
"strictness": "high",
|
|
51
|
+
"expected_behavior": [
|
|
52
|
+
{
|
|
53
|
+
"grader": "judge",
|
|
54
|
+
"rubric": "A correct answer states that the current test only proves dispose() doesn't throw, not that the autorun actually stops firing, and fixes it by disposing the store, then triggering the mutation the autorun depends on, and asserting the autorun's effect did not run again.",
|
|
55
|
+
"pass_criteria": [
|
|
56
|
+
"States that the current test only proves dispose() runs without throwing, not that the autorun's effect actually stops firing after disposal",
|
|
57
|
+
"Shows or names a corrected test that calls dispose(), then mutates the observable the autorun depends on, then asserts the autorun's tracked effect (e.g. a spy/counter it increments) did not run again"
|
|
58
|
+
],
|
|
59
|
+
"fail_criteria": [
|
|
60
|
+
"Claims the existing test (calling dispose() alone with no post-dispose mutation or assertion) is already sufficient to prove the autorun stops running",
|
|
61
|
+
"Recommends asserting only that store.disposed is set to true as proof the reaction stopped, without triggering the dependent mutation and checking the reaction's own effect did not re-run"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"calibration": {
|
|
66
|
+
"known_right": "No -- calling dispose() and asserting nothing else only proves dispose() doesn't throw. It says nothing about whether the autorun's own effect actually stopped running afterward. To prove that, trigger the mutation the autorun depends on after disposal and check its effect didn't fire again:\n\n```ts\nit(\"stops the autorun after dispose\", () => {\n const spy = vi.fn();\n const store = new ItemStore({ onCountChange: spy }); // autorun calls onCountChange internally\n store.disposeCallCount = 0;\n\n store.dispose();\n runInAction(() => {\n store.items.push(newItem()); // would normally trigger the autorun\n });\n\n expect(spy).not.toHaveBeenCalled();\n});\n```\n\nThe key part is the mutation *after* dispose() and the assertion that the tracked effect did not run again -- without both, the test can't actually distinguish a correctly-disposed reaction from one that was never wired up correctly in the first place.",
|
|
67
|
+
"known_wrong": "The current test is fine -- if dispose() didn't clean up the autorun's internal MobX subscription correctly, calling it would throw an error from MobX's reaction system. Since it completes without throwing, that's good evidence the disposer ran and the reaction was torn down properly. No need to add a post-dispose mutation check.",
|
|
68
|
+
"vague": "That test probably isn't strong enough -- you should check that the autorun really doesn't keep running after dispose, not just that dispose itself works.",
|
|
69
|
+
"subtle_wrong": "Good catch -- I strengthened it by asserting `store.disposed` is `true` right after calling dispose(): `store.dispose(); expect(store.disposed).toBe(true);`. That confirms the store's own disposed flag flipped, which is what the autorun's guard checks internally, so if disposed is true the autorun's guarded logic won't run on the next reaction anyway."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|