solid2-agent-kit 0.4.2 → 0.4.4

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 CHANGED
@@ -16,7 +16,7 @@ Supported agents: **Cursor** and **Claude Code**.
16
16
  | Always-applied hard rules | `.cursor/rules/solid-2.mdc` (glob-attached to `*.tsx`/`*.jsx`) | managed block in `CLAUDE.md` | 23 hard rules + banned Solid 1.x API table + eslint-plugin-solid lint-heritage table. Injected without the agent having to decide to read anything |
17
17
  | Agent skill | `.cursor/skills/solid-2/` | `.claude/skills/solid-2/` | Execution-model primer, decision tables, canonical patterns, a “still runs / write the other form” table, review checklist, official-doc URL index |
18
18
  | Shared agent context | managed block in `AGENTS.md` | same (Claude Code users can reference it from `CLAUDE.md`) | Core principles and pointers, always in context |
19
- | Mechanical gate | `solid2-kit check` | same | Whole-file regex guard over `src/**/*.{ts,tsx}`: fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`, Solid 1.x imports/APIs/components/JSX namespaces, `vite-plugin-solid`, old-router `<Route>`/`<HashRouter>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props, `value()!` hand-narrowing, hand-rolled loading signals, and `=== undefined` readiness branches |
19
+ | Mechanical gate | `solid2-kit check` | same | Whole-file regex guard over `src/**/*.{ts,tsx}`: fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`/`useOptimistic`/`onDoubleClick`, Solid 1.x imports/APIs/components/JSX namespaces, `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props, `value()!` hand-narrowing, hand-rolled loading signals, `=== undefined` readiness branches, SolidStart leftovers (`@solidjs/start`, `h3`, `createAsync`, `useSubmission`, `"use client"`), Next.js imports/`useRouter`/`typeof window`, `history.pushState`, and `render(<App />)` (pass a function) |
20
20
 
21
21
  Managed blocks are delimited with `<!-- solid2-agent-kit:*:start/end -->` markers; everything
22
22
  outside them is yours. Kit-owned files (`solid-2.mdc`, the skill directories) are overwritten
@@ -24,8 +24,8 @@ on sync.
24
24
 
25
25
  ## Usage
26
26
 
27
- GitHub is the canonical distribution channel (the kit is intentionally not published to
28
- npm; consumers pin a commit through their lockfile):
27
+ GitHub is the canonical distribution channel (consumers pin a commit through their
28
+ lockfile); the package is also published to npm as `solid2-agent-kit`:
29
29
 
30
30
  ```sh
31
31
  # install guidance for both Cursor and Claude Code (default)
@@ -55,8 +55,8 @@ resolve the project-local bin, so the bare name is safe there):
55
55
  ```
56
56
 
57
57
  Update later with `bun update solid2-agent-kit` (or the npm/pnpm equivalent) followed by
58
- `npx solid2-agent-kit sync`. Outside a project that has the kit installed, always use the
59
- `npx github:...` form — the bare package name is not claimed on the npm registry.
58
+ `npx solid2-agent-kit sync`. Outside a project that has the kit installed, prefer the
59
+ `npx github:...` form — it always tracks this repo directly.
60
60
 
61
61
  ## Why not eslint-plugin-solid?
62
62
 
@@ -96,6 +96,14 @@ scripts/check-docs-drift.mjs weekly docs cross-check
96
96
  tests/ mechanical-gate fixtures (`npm test`)
97
97
  ```
98
98
 
99
+ ## Publishing (npm)
100
+
101
+ Bump `version` in `package.json` on `main` and CI publishes automatically
102
+ (`.github/workflows/publish.yml`) — it skips when the version already exists on npm.
103
+ Auth is npm Trusted Publishing (OIDC), so no NPM_TOKEN secret exists or is needed.
104
+ One-time setup on npm: package → Settings → Trusted Publisher → GitHub Actions, owner
105
+ `lightsound`, repo `solid2-agent-kit`, workflow `publish.yml`, environment left empty.
106
+
99
107
  ## License
100
108
 
101
109
  MIT
@@ -177,7 +177,7 @@ const CHECKS = [
177
177
  {
178
178
  id: 'react-hook',
179
179
  pattern:
180
- /(?<![.\w])use(?:State|Effect|Memo|Ref|Callback|Reducer|LayoutEffect|Transition|SyncExternalStore|ImperativeHandle|Id|DeferredValue|InsertionEffect)\s*\(/g,
180
+ /(?<![.\w])use(?:State|Effect|Memo|Ref|Callback|Reducer|LayoutEffect|Transition|SyncExternalStore|ImperativeHandle|Id|DeferredValue|InsertionEffect|Optimistic|ActionState|FormStatus)\s*\(/g,
181
181
  message: 'React hook. See the primitive mapping in the solid-2 rules.',
182
182
  },
183
183
  {
@@ -281,15 +281,68 @@ const CHECKS = [
281
281
  },
282
282
  {
283
283
  id: 'solid1-router',
284
- pattern: /<(?:HashRouter|MemoryRouter|Route|Navigate)\b/g,
284
+ pattern: /<(?:HashRouter|MemoryRouter|Route|Navigate|A|FileRoutes|StartClient|StartServer)\b/g,
285
285
  message:
286
- 'Solid Router 0.x/1.x JSX. Define routes with createRouter({ routes }) and plain <a href={Router.paths...}>.',
286
+ 'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}>.',
287
287
  },
288
288
  {
289
289
  id: 'meta-provider',
290
290
  pattern: /<MetaProvider\b/g,
291
291
  message: 'Solid Meta 1.x has no provider. Render <Title>/<Meta>/<Link> from "@solidjs/meta" anywhere.',
292
292
  },
293
+ {
294
+ id: 'solidstart-import',
295
+ pattern: /from\s+['"](?:@solidjs\/start|vinxi|h3|nitropack)(?:['"]|\/)/g,
296
+ message:
297
+ 'SolidStart leftover. Import GET from "@solidjs/web/server-functions"; routes are createRouter / fileRoutes(pageRoutes), not @solidjs/start, vinxi, h3, or nitropack.',
298
+ },
299
+ {
300
+ id: 'solidstart-api',
301
+ pattern: /(?<![.\w])(?:createAsync|createAsyncStore|useSubmission|defineEventHandler)\s*\(/g,
302
+ message:
303
+ 'SolidStart / Router 0.x leftover. Read query() through createMemo(() => getUser(id())); settled results via useSubmissions (plural); replace cache()/json() with query/respond; rewrite H3 handlers against Request/Response.',
304
+ },
305
+ {
306
+ id: 'use-client',
307
+ pattern: /['"]use client['"]/g,
308
+ message: 'React/Next client directive. Solid has no "use client"; server functions use "use server" only.',
309
+ },
310
+ {
311
+ id: 'next-import',
312
+ pattern: /from\s+['"]next(?:['"]|\/)/g,
313
+ message: 'Next.js import in a Solid 2.0 codebase. Use @solidjs/router, @solidjs/meta, and @solidjs/web.',
314
+ },
315
+ {
316
+ id: 'render-jsx-element',
317
+ pattern: /\b(?:render|hydrate|renderToString|renderToStream)\(\s*</g,
318
+ message:
319
+ 'Pass a function to render/hydrate/renderToString/renderToStream so Solid creates the root first: render(() => <App />, root).',
320
+ },
321
+ {
322
+ id: 'typeof-window',
323
+ pattern: /typeof\s+window\b/g,
324
+ message:
325
+ '`typeof window` is not the SSR boundary. Use isServer / isDev from "@solidjs/web", or clientOnly(() => import("./Widget")).',
326
+ },
327
+ {
328
+ id: 'next-nav',
329
+ pattern:
330
+ /(?<![.\w])(?:useRouter|usePathname|revalidatePath|revalidateTag|notFound|hydrateRoot)\s*\(/g,
331
+ message:
332
+ 'Next.js / React DOM leftover. In-app navigation is useNavigate / <a href={Router.paths...}>; 404 is httpStatus(404); hydrate is hydrate(() => <App />, root).',
333
+ },
334
+ {
335
+ id: 'react-dom-event',
336
+ pattern: /\b(?:onDoubleClick|onKeyPress|suppressHydrationWarning)=/g,
337
+ message:
338
+ 'React DOM prop. Native dblclick is onDblClick; keypress is onKeyDown; Solid has no suppressHydrationWarning.',
339
+ },
340
+ {
341
+ id: 'history-nav',
342
+ pattern: /\b(?:history\.pushState|history\.replaceState|window\.location\.href\s*=)/g,
343
+ message:
344
+ 'Bypass of the router. In-app navigation is useNavigate() or <a href={Router.paths...}>.',
345
+ },
293
346
  {
294
347
  // A zero-arg call compared against undefined/null is a manual async
295
348
  // readiness branch (`data() === undefined ? ... : ...`).
@@ -3,4 +3,4 @@
3
3
  - All TSX in this project is Solid 2.0. Components run **once**; reactivity flows through signals/stores to JSX. React patterns (props destructuring, `className`, per-keystroke `onChange`, state-synced-by-effect, `.map()` lists) and Solid 1.x APIs (`createResource`, `onMount`, `solid-js/store`, `Suspense`, path setters) are bugs here.
4
4
  - Before writing TSX, read the `solid-2` skill (`.cursor/skills/solid-2/SKILL.md` for Cursor, `.claude/skills/solid-2/SKILL.md` for Claude Code): patterns, decision tables, official doc URLs. Hard rules auto-attach from `.cursor/rules/solid-2.mdc` in Cursor and live in a managed block in `CLAUDE.md` for Claude Code.
5
5
  - After editing TSX, run the `solid2-kit check` mechanical gate — it fails on React/Solid 1.x tokens and props destructuring. Use the project's `lint:solid` script if one exists; otherwise `npx solid2-agent-kit check` (with the kit as a devDependency) or `npx github:lightsound/solid2-agent-kit check`. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
6
- - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`.
6
+ - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`.
@@ -33,8 +33,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
33
33
  as an updater.
34
34
  5. `class`, not `className`; `for`, not `htmlFor`. `class` accepts strings, conditional
35
35
  objects, and nested arrays: `class={["btn", { active: selected() }]}`. Put *conditional*
36
- names in an object — `class={`btn ${on() ? "on" : ""}`}` works but rebuilds the whole
37
- string, so Solid cannot add/remove one token. `style` objects use CSS property names
36
+ names in an object — `class={`btn ${on() ? "on" : ""}`}`, `clsx(...)`, and
37
+ `.filter(Boolean).join(" ")` all work but rebuild the whole string, so Solid cannot
38
+ add/remove one token. `style` objects use CSS property names
38
39
  (`"background-color"`, not `backgroundColor`) and numbers do NOT auto-append `px`
39
40
  (`width: `${n}px``).
40
41
  6. **`onInput` for per-keystroke handling.** Solid's `onChange` is the native change event
@@ -77,23 +78,35 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
77
78
  accessor) only when that identity change should show the fallback again. Pass an async
78
79
  memo to a child as the **accessor** (`user={user}`), and read `props.user()` under that
79
80
  child's `<Loading>` — `user={user()}` throws not-ready at the parent, so the child's
80
- boundary never sees it. "Prop with local edits" = writable derivation:
81
+ boundary never sees it. Do not `try/catch` `NotReadyError` around a read, and do not
82
+ use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
83
+ `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer (`isPending`
84
+ is the indicator). "Prop with local edits" = writable derivation:
81
85
  `createSignal(() => props.value)` or `createStore(() => props.value, fallback)`.
86
+ `createOptimistic` is for an in-flight mutation, not a local editing session.
82
87
  11. Context: the context object is the provider — `<MyContext value={{ theme, setTheme }}>`
83
88
  (no `.Provider`). Pass accessors/setters/stores through context, never snapshot values.
84
89
  `createContext<T>()` without a default throws when read outside a provider (good) — use
85
90
  that for any reactive payload. `createContext("light")` is only for a primitive static
86
91
  fallback; a dummy default on a store/signal context *runs* and then silently no-ops
87
- without a provider.
92
+ without a provider. Truly app-wide singletons (theme, session, locale) are a
93
+ **module-level** signal/store — Context is subtree scoping, not a React-style
94
+ app store. Module state is shared across SSR requests.
88
95
  12. Refs: `let el!: HTMLDivElement` + `ref={el}` or `ref={(node) => (el = node)}`, or forward
89
96
  `props.ref`. Compose with arrays: `ref={[props.ref, (node) => (el = node)]}`. No
90
97
  `useRef`/`.current`. Ref callbacks run untracked and **without an owner**; their
91
98
  return values are ignored. Do not create effects or register cleanup inside a ref
92
99
  callback. For reusable directives, create owned primitives (`onSettled`) in a
93
- factory and return only the element callback.
100
+ factory and return only the element callback. Component setup/teardown is
101
+ `onSettled(() => { ...; return cleanup })` — `onCleanup` is for custom
102
+ primitives and captured owners, not component bodies. Each `onSettled`
103
+ registers a **single** fire (reads inside are untracked); ongoing
104
+ imperative work is `createEffect`.
94
105
  13. **Writes are staged** and commit on the next microtask. Event handlers need nothing
95
106
  special; tests and imperative integration code must call `flush()` before observing
96
- updated state or DOM. Never call `flush()` inside an `action`.
107
+ updated *synchronous* state or DOM. Waiting on an async memo is
108
+ `await resolve(() => value())` (or Testing Library async queries), not `flush()`.
109
+ Never call `flush()` inside an `action`.
97
110
  14. Do not port these React tools — they have no Solid equivalent because the problems they
98
111
  solve don't exist: `useCallback`, `React.memo`, `forwardRef`, `useSyncExternalStore`,
99
112
  dependency arrays, `startTransition`/`useTransition` (updates are held and coordinated
@@ -101,7 +114,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
101
114
  15. **Components return once.** Never early-return or conditionally return based on reactive
102
115
  values — the branch is chosen once at setup and frozen forever. Put conditionals inside
103
116
  JSX (`<Show>`, ternary, `<Switch>`/`<Match>`). Early returns on genuinely non-reactive
104
- values (build-time config, missing env) are fine.
117
+ values (build-time config, missing env) are fine. `<Switch>` is first-wins: later
118
+ truthy `<Match>`es are skipped, not “all matching branches”.
105
119
  16. Prefer `textContent` for text-only content. Use `innerHTML` only for trusted or
106
120
  sanitized markup — never interpolate user input into it, never React's
107
121
  `dangerouslySetInnerHTML`, and never combine `innerHTML` with JSX children (they fight
@@ -111,6 +125,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
111
125
  **narrowed accessor**, so no `!` is needed:
112
126
  `<Show when={error()}>{(err) => <p>{err().message}</p>}</Show>`. For non-reactive
113
127
  zero-arg calls, use an explicit guard variable instead of `!`.
128
+ Do not add `keyed` by default (the React `key` reflex) — default `Show`/`Match`
129
+ keep children mounted; `keyed` remounts when identity changes.
114
130
  Mechanically enforced by `solid2-kit check`.
115
131
  18. **Never hand-roll async UI state.** No `[loading, setLoading]` signals, no
116
132
  `data() === undefined` readiness branches, no `{ data, error }` signal pairs from
@@ -143,10 +159,25 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
143
159
  state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
144
160
  Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`). Server vs browser:
145
161
  `isServer` / `isDev` from `@solidjs/web`, or `clientOnly(() => import("./Widget"))` for
146
- browser-only components — never `typeof window` as the SSR boundary. `JSON.stringify(store)`
162
+ browser-only components — never `typeof window` as the SSR boundary. Default
163
+ `clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
164
+ render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
165
+ visible content — `clientOnly` is for components that must never run on the server.
166
+ Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
167
+ (`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
168
+ container already holds SSR HTML; `render` for an empty mount. `renderToString`
169
+ emits `<Loading>` fallbacks; async or lazy-route trees need
170
+ `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`).
171
+ When the app owns the document, emit `<HydrationScript />` once before app markup;
172
+ extra roots get a distinct `renderId`. Import `GET` from
173
+ `@solidjs/web/server-functions`, never `@solidjs/start`. `httpStatus` /
174
+ `httpHeader` are scope *declarations* (call them bare in a component or
175
+ fallback body), not event-time mutations. `JSON.stringify(store)`
147
176
  / `structuredClone(store)` without `snapshot(store)` can throw or leak proxies. Async SSR
148
177
  without `<Loading>` *works* but blocks the HTML shell until every read settles — wrap
149
178
  reads, or stream with `renderToStream`.
179
+ Navigation-shaped updates (a setter, then async computeds holding previous values)
180
+ do not need core `action` — reach for it only when writes happen *after* async work.
150
181
  23. **Composition.** Pass `props.children` through when you only render them. Inspect or
151
182
  iterate children with `children(() => props.children)` (then `.toArray()`), never a
152
183
  setup-time snapshot. Code-split with `lazy(() => import("./X"))` under `<Loading>` —
@@ -155,7 +186,20 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
155
186
  because the named export is not a call-site literal. Select a component from reactive
156
187
  state with `dynamic(() => ...)` from `@solidjs/web`
157
188
  (stable identity). If the project has `@solidjs/router`, routes are `createRouter({ routes })`
158
- at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. Document head is
189
+ at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
190
+ `query` are URL-addressable POST forms and a read cache — not core `action` /
191
+ `refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
192
+ (POST only; `.with(id)` binds args into the URL). `useAction` is JS-only. Cache
193
+ reads: `query(fn, "name")` then `createMemo(() => getUser(id()))`; after a mutation
194
+ `revalidate(getUser.key)`. Do not wrap mutations in `query` (an undeclared server
195
+ function becomes GET). `Router.paths` is not the current location (`useLocation` /
196
+ `useParams`); a route `preload` result is `props.data`. One router instance —
197
+ Solid Router does not support nested `<Router>` / nested `createRouter`.
198
+ In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
199
+ `window.location` / `history.pushState`. Trusted identity is
200
+ `getRequestEvent()`, never a caller-supplied user id. Unscripted forms use
201
+ the function `.url` (or a router `action`), not a hand-built `/_server/`
202
+ path. Document head is
159
203
  `<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`.
160
204
 
161
205
  ## Lint heritage: eslint-plugin-solid (Solid 1.x) intents carried into this file
@@ -208,7 +252,10 @@ directory (default `src/`).
208
252
  | `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
209
253
  | `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
210
254
  | `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
211
- | JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` | `createRouter({ routes })`, plain `<a href={Router.paths...}>`, `hashHistory()` |
255
+ | JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)`, plain `<a href={Router.paths...}>`, `hashHistory()` |
256
+ | `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
257
+ | `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
258
+ | `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
212
259
  | `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
213
260
  | `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive |
214
261
  | `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
@@ -68,7 +68,7 @@ Most React `useEffect` code should NOT become `createEffect`:
68
68
  | Code-split a component | `lazy(() => import("./X"))` read under `<Loading>` |
69
69
  | Named export from a lazy module | `lazy(() => import("./pages"), { export: "About" })` |
70
70
  | Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity) |
71
- | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` |
71
+ | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
72
72
  | Server vs browser branch | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` |
73
73
  | SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
74
74
 
@@ -149,7 +149,8 @@ the component must **inspect or iterate** them. It returns an accessor with
149
149
  `.toArray()`. The helper call belongs in the component body; the read of
150
150
  `props.children` is inside the accessor (a tracking scope), so this is not a
151
151
  setup-time snapshot. Do not assign `const kids = props.children` and do not
152
- treat resolved children as a reactive data list for `<For>`:
152
+ treat resolved children as a reactive data list for `<For>` —
153
+ render `{resolved.toArray()}`, not `<For each={resolved()}>`:
153
154
 
154
155
  ```tsx
155
156
  import { children, type ParentProps } from 'solid-js';
@@ -197,7 +198,8 @@ const Result = dynamic(() => (detailed() ? Detailed : Compact));
197
198
 
198
199
  `dynamic()` returns a **stable** component whose source can be a component, an
199
200
  intrinsic tag name, a promise, or empty. Prefer it over swapping a component
200
- variable in JSX. `<Dynamic>` is the JSX spelling of the same primitive.
201
+ variable in JSX (`const View = tab() ? A : B` freezes the choice at setup).
202
+ `<Dynamic>` is the JSX spelling of the same primitive.
201
203
 
202
204
  ### Two-phase effect (imperative boundary only)
203
205
 
@@ -287,7 +289,8 @@ function createSubscriptionQuery<T>(
287
289
  const queue: T[] = [];
288
290
  let failure: unknown;
289
291
  let wake = () => {};
290
- // Component-owned setup: unsubscribe is tied to the caller's owner.
292
+ // Custom primitive: unsubscribe is tied to the caller's owner via onCleanup.
293
+ // Component bodies use onSettled and return cleanup instead.
291
294
  onCleanup(subscribe(
292
295
  (value) => { queue.push(value); wake(); },
293
296
  (error) => { failure = error; wake(); },
@@ -320,11 +323,23 @@ const addTodo = action(function* (todo: Todo) {
320
323
 
321
324
  Ordinary signal/store writes inside an action are held until it settles. Never call
322
325
  `flush()` inside an action. Invoke actions from handlers, not from component/computation bodies.
326
+ Do not set a `submitted` signal and watch it from an effect — that work belongs in the
327
+ handler or the `action`.
328
+
329
+ `createOptimistic` / `createOptimisticStore` are for a tentative value during an
330
+ active mutation, not a local editing session (use a writable derivation or a plain
331
+ store for that).
323
332
 
324
333
  **`yield`, not a bare `await`, is the transaction boundary.** `await api.save()` then
325
334
  `setTodos(...)` *runs*, but the write commits immediately and optimistic rollback is lost.
326
335
  Either `yield saveTodo(todo)` or, when you need a typed result, `const saved = await api.save(); yield; setTodos(...)`.
327
336
 
337
+ Navigation-shaped updates do **not** need core `action`. A plain setter is enough:
338
+ reads pull the async, and downstream async computeds hold previous values until the
339
+ new ones are ready (`isPending` / `latest`). Reach for `action` from `solid-js` only
340
+ when writes happen *after* async work. Router form `action` from `@solidjs/router` is
341
+ a different API (URL + POST) — see [App stack](#app-stack-only-if-the-project-has-these-packages).
342
+
328
343
  ### Lists: `<For>` child signatures per keying mode
329
344
 
330
345
  ```tsx
@@ -380,7 +395,8 @@ Plain ternaries remain fine when no narrowing is involved (`{open() ? <A /> : <B
380
395
  Default `Show` keeps children mounted across truthy changes. Add `keyed` to remount when the
381
396
  value's identity changes (child then receives the raw value, like React's `key` reset).
382
397
  Prefer the default; use `keyed` only when internal state must reset. Multi-branch:
383
- `<Switch>` / `<Match>`.
398
+ `<Switch>` / `<Match>` — **first truthy `<Match>` wins**; later matches are skipped,
399
+ even if they are also truthy.
384
400
 
385
401
  Components must **return once**: never early-return based on a reactive value — the branch
386
402
  is picked at setup and frozen. Early returns on non-reactive values (build-time config, a
@@ -426,7 +442,9 @@ function ThemeButton() {
426
442
 
427
443
  Pass accessors/setters/stores through context — never `value={theme()}` (a dead snapshot).
428
444
  No `useMemo` for the value object, no context splitting: the object is created once and
429
- fine-grained updates flow through the signals inside it.
445
+ fine-grained updates flow through the signals inside it. Context is **subtree scoping**.
446
+ A true app-wide singleton (theme, session, locale) is a module-level signal/store —
447
+ do not invent a root provider for that. Module state is shared across SSR requests.
430
448
 
431
449
  ### Refs
432
450
 
@@ -479,7 +497,9 @@ expect(count()).toBe(2);
479
497
  ```
480
498
 
481
499
  Wait for an async expression to settle in tests: `await resolve(() => value())`.
482
- Run tests in dev mode first — Solid 2 emits diagnostics for top-level reactive reads,
500
+ `flush()` only drains staged **synchronous** writes — it is not a stand-in for waiting
501
+ on a pending resource. `onSettled` is also a **single** fire (untracked): it is not
502
+ `onMount` that re-runs, and it is not an effect. Run tests in dev mode first — Solid 2 emits diagnostics for top-level reactive reads,
483
503
  writes from owned scopes, and async reads outside `<Loading>`. Fix them; don't suppress.
484
504
 
485
505
  Component tests use `@solidjs/testing-library`. Pass a **function** to `render` so
@@ -554,10 +574,69 @@ export default function App() {
554
574
  ```
555
575
 
556
576
  Create the instance at **module scope**. Nested layouts are `children` arrays
557
- on the route objects, not nested `<Route>` / nested routers. Navigate with
577
+ on the route objects, not nested `<Route>` / nested routers. Solid Router does
578
+ **not** support nested `<Router>` instances — compose one route tree (or a lazy
579
+ `children` thunk). Navigate with
558
580
  plain `<a href={Router.paths.about}>` (or `useNavigate`); there is no `<A>` /
559
- `<Navigate>` / `<HashRouter>`. Session location is `useLocation` / `useParams`,
560
- not `Router.paths`. One router per app.
581
+ `<Navigate>` / `<HashRouter>` / `<FileRoutes />`. Do not assign
582
+ `window.location.href` or call `history.pushState` for in-app navigation.
583
+ Session location is `useLocation` / `useParams`,
584
+ not `Router.paths`. A route `preload` result is `props.data` on the matched
585
+ component (and the factory `preload` result is the root render prop's `props.data`).
586
+ Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks: false`
587
+ disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
588
+ `"native"` / `"preload"`. One router per app.
589
+
590
+ **Two different `action`s.** Core `action` from `solid-js` is a generator transaction
591
+ (optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
592
+ a URL-addressable POST mutation (`useSubmissions`, `<form action={save} method="post">`).
593
+ Do not import one and use it as the other.
594
+
595
+ ```tsx
596
+ import { action } from '@solidjs/router';
597
+ import { redirect } from '@solidjs/web';
598
+
599
+ const save = action(async (form: FormData) => {
600
+ await saveProfile(form);
601
+ return redirect('/account');
602
+ }, 'update-profile');
603
+
604
+ <form action={save} method="post">
605
+ <input name="displayName" />
606
+ <button type="submit">Save</button>
607
+ </form>
608
+ ```
609
+
610
+ Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
611
+ action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
612
+ drops the no-JS form fallback. `useAction` is JS-only for the same reason.
613
+
614
+ **`query` is the read cache** (optional, only with the router). Wrap a read,
615
+ give it a stable name, and consume it through a Solid async primitive:
616
+
617
+ ```tsx
618
+ import { createMemo } from 'solid-js';
619
+ import { query, revalidate } from '@solidjs/router';
620
+
621
+ export const getUser = query(async (id: string) => {
622
+ const response = await fetch(`/api/users/${id}`);
623
+ return response.json();
624
+ }, 'users');
625
+
626
+ const user = createMemo(() => getUser(params.id));
627
+ // after a mutation:
628
+ revalidate(getUser.key);
629
+ revalidate(getUser.keyFor('42'));
630
+ ```
631
+
632
+ A bare `createMemo(async () => fetchUser(id()))` still works; it just has no
633
+ shared cache, no preload reuse, and no `revalidate`. Do not wrap mutations in
634
+ `query` — wrapping an undeclared server function makes it GET. `query.get(key)`
635
+ throws if there is no entry; `query.set` does not accept a promise. Cache is
636
+ request-scoped on the server and application-scoped in the browser. After a
637
+ router mutation, `revalidate(...)` — not core `refresh()`. Replace SolidStart /
638
+ Router 0.x leftovers: no `createAsync`, `useSubmission` (singular), `cache()`,
639
+ router `json()`, or `<FileRoutes />` (`fileRoutes(pageRoutes)` instead).
561
640
 
562
641
  ### Server functions — `"use server"`
563
642
 
@@ -570,16 +649,22 @@ export async function findUser(id: string) {
570
649
  ```
571
650
 
572
651
  A function-level server function cannot close over component locals — pass
573
- values as arguments. Treat every argument as untrusted. During SSR the same
652
+ values as arguments. Treat every argument as untrusted. Read trusted identity
653
+ from `getRequestEvent()`, never from a caller-supplied user id. During SSR the same
574
654
  reference runs in-process; in the browser it is HTTP. Mutations that should
575
655
  revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
576
656
  guides). For 404 during SSR: `httpStatus(404)` from `@solidjs/web`.
577
657
  `throw new Error("…")` is stripped to `"Internal Server Error"` in production —
578
658
  use `markSafeError` or `respond(..., { status })` for intentional client-facing
579
- failures. `GET()` is only for idempotent reads (URLs leak into logs/history).
659
+ failures. `GET()` is only for idempotent reads (URLs leak into logs/history);
660
+ import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
580
661
  `live()` yields **current state** (each yield replaces the last); do not treat
581
662
  it as an append-only event log, and hoist the memo so consumers share one
582
663
  connection.
664
+ Unscripted forms: `<form method="post" action={createTodo.url}>` (the reference
665
+ `.url`), not a client `preventDefault` + `fetch`, and not a hand-built
666
+ `/_server/` URL. GET forms (`method="get" action={search.url}`) are only for
667
+ idempotent search — do not use a GET form for a mutation.
583
668
 
584
669
  ### Document head — no MetaProvider
585
670
 
@@ -604,7 +689,7 @@ how to reuse. Prefer the form on the right.
604
689
  |---|---|
605
690
  | `{todos().map((t) => <Row todo={t} />)}` | `<For each={todos()} keyed={(t) => t.id}>` |
606
691
  | `<For each={todos().map(t => t)}>` | derive first, then `<For each={visible()}>` |
607
- | `class={`btn ${on() ? "on" : ""}`}` | `class={["btn", { on: on() }]}` |
692
+ | `class={`btn ${on() ? "on" : ""}`}` / `clsx("btn", on() && "on")` / `.filter(Boolean).join(" ")` | `class={["btn", { on: on() }]}` |
608
693
  | `style={{ width: 80 }}` (no unit) | `style={{ width: `${80}px` }}` |
609
694
  | `const rest = { ...props }` | `const rest = omit(props, "label")` |
610
695
  | `const user = createMemo(async () => { await fetch(...); return id(); })` | read `id()` **before** `await` |
@@ -632,6 +717,42 @@ how to reuse. Prefer the form on the right.
632
717
  | `live()` yields as an event log to append | each yield **replaces** the current answer; yield current state first |
633
718
  | `<article innerHTML={html()}>…children…</article>` | `innerHTML` **or** children, not both |
634
719
  | `fallback={(error) => <p>{error.message}</p>}` | `error` is an accessor: `error().message` (or `String(error())`) |
720
+ | `import { action } from "solid-js"` on a `<form>` | `import { action } from "@solidjs/router"` + `<form action={save} method="post">` (core `action` is a generator transaction, not a form URL) |
721
+ | `createMemo(async () => fetchUser(id()))` when `@solidjs/router` is present | `query(fetchUser, "users")` + `createMemo(() => getUser(id()))`; after mutations `revalidate(getUser.key)`, not core `refresh()` |
722
+ | `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` for a router/server action | `<form action={save} method="post">` (POST only; `.with(id)` binds args into the URL). `useAction` is JS-only |
723
+ | `query(saveUser, "users")` wrapping a mutation | don't wrap mutations in `query` — an undeclared server function becomes GET |
724
+ | `render(<App />, root)` / Testing Library `render(<Counter />)` | `render(() => <App />, root)` — pass a function so Solid creates the root first |
725
+ | `render` onto SSR HTML / `hydrate` into an empty node | `hydrate(() => <App />, root)` when HTML already exists; `render` for an empty mount |
726
+ | `renderToString(() => <App />)` for an async or lazy-route tree | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`). String render emits `<Loading>` fallbacks |
727
+ | `clientOnly(() => import("./Map"))` for a rarely shown widget | same + `{ lazy: true }` so the import waits until first render (default starts at declaration) |
728
+ | `<NoHydration><Chart /></NoHydration>` to skip SSR of a widget | `clientOnly(() => import("./Chart"))` — `NoHydration`/`Hydration` split ownership, they do not choose visible content |
729
+ | `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
730
+ | `action(function* () { setX(v); })` around a navigation-shaped setter | plain setter; async computeds hold previous values. Core `action` only when writes happen *after* async work |
731
+ | `createContext` + a root provider for a true app singleton | module-level signal/store (SSR: shared across requests). Context is subtree scoping |
732
+ | `import { GET } from "@solidjs/start"` / `createAsync` / `useSubmission` / `<FileRoutes />` / `"use client"` | `GET` from `@solidjs/web/server-functions`; `createMemo(() => getUser(id()))`; `useSubmissions`; `fileRoutes(pageRoutes)`; Solid has no `"use client"` |
733
+ | `<Show keyed when={user()}>` by default | default `Show` (keeps children mounted); `keyed` only when internal state must reset |
734
+ | overlapping truthy `<Match>`es all expected to render | first truthy `<Match>` wins; later matches are skipped |
735
+ | missing `<HydrationScript />` / one script for many roots | once, before app markup, when the app owns the document; distinct `renderId` per extra root |
736
+ | `Router.paths` as the current URL; calling `preload()` and using the return as props | `useLocation` / `useParams`; factory/route `preload` result is `props.data` |
737
+ | `query.get(key)` with no guaranteed entry; `query.set(key, promise)` | `query.get` throws if missing; `query.set` does not accept a promise |
738
+ | `onCleanup(() => ...)` in a component body | `onSettled(() => { ...; return cleanup })` — `onCleanup` is for custom primitives |
739
+ | `onSettled` with reactive reads expecting re-runs | each call is a **single** fire; reads are untracked. Ongoing work is `createEffect` |
740
+ | `<Router>` nested inside `<Router>` | one instance; nest `children` arrays (or a lazy children thunk) |
741
+ | `const View = tab() ? A : B; return <View />` | `const View = dynamic(() => (tab() ? A : B))` — the ternary at setup freezes the choice |
742
+ | `<For each={resolved()}>` after `children()` | `{resolved.toArray()}` — resolved children are not a reactive data list |
743
+ | `<input onChange={...}>` for keystrokes | `onInput` — native `onChange` is blur/commit |
744
+ | `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
745
+ | `createMemo(async fn, { loadingValue })` as the default first-flight UI | `<Loading>` for first flight; `loadingValue` is an escape hatch |
746
+ | `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
747
+ | `findUser(userId)` with the id from the client as identity | `getRequestEvent()!.locals.userId` |
748
+ | `action={"/_server/" + id}` in app JSX | `action={createTodo.url}` or a router `action` |
749
+ | `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
750
+ | `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
751
+ | `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
752
+ | `try { user() } catch (e) { /* NotReadyError */ }` | `<Loading>` — app code should not catch `NotReadyError` |
753
+ | `{latest(() => results())}` as the visible list | settled read + `isPending()` for the indicator; `latest` is for previews |
754
+ | `typeof window !== "undefined"` | `isServer` / `isDev` from `@solidjs/web` |
755
+ | `Object.assign({}, props)` / expecting `merge` to skip `undefined` like 1.x `mergeProps` | `omit` / `merge` — explicit `undefined` **overrides** (Object.assign) |
635
756
 
636
757
  ```tsx
637
758
  // WRONG — post-await read never subscribes; production can hang pending
@@ -684,6 +805,43 @@ function Page() {
684
805
  }
685
806
  ```
686
807
 
808
+ ```tsx
809
+ // WRONG — two different `action`s; the core one is not a form URL
810
+ import { action } from 'solid-js';
811
+ const save = action(async function* (form: FormData) {
812
+ yield persist(form);
813
+ });
814
+ <form onSubmit={(e) => { e.preventDefault(); void save(new FormData(e.currentTarget)); }}>
815
+ <button type="submit">Save</button>
816
+ </form>
817
+
818
+ // CORRECT — router action is POST-addressable (only if `@solidjs/router` is in the project)
819
+ import { action } from '@solidjs/router';
820
+ const save = action(async (form: FormData) => persist(form), 'save');
821
+ <form action={save} method="post">
822
+ <button type="submit">Save</button>
823
+ </form>
824
+ ```
825
+
826
+ ```tsx
827
+ // WRONG — evaluating JSX before the root exists
828
+ import { render } from '@solidjs/web';
829
+ render(<App />, document.getElementById('app')!);
830
+
831
+ // CORRECT
832
+ render(() => <App />, document.getElementById('app')!);
833
+ ```
834
+
835
+ ```tsx
836
+ // WRONG — tab() is read once at setup; View never changes
837
+ const View = tab() ? Detailed : Compact;
838
+ return <View value="now" />;
839
+
840
+ // CORRECT
841
+ const View = dynamic(() => (tab() ? Detailed : Compact));
842
+ return <View value="now" />;
843
+ ```
844
+
687
845
  ## Checking the official docs
688
846
 
689
847
  Solid 2.0 docs: https://v2.solidjs.com/ — the site blocks non-browser fetchers, so from an
@@ -725,7 +883,14 @@ always-applied rules installed alongside this skill.
725
883
  - [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
726
884
  component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
727
885
  - [ ] Browser-only code uses `isServer` / `clientOnly`, not `typeof window`.
886
+ Component teardown is `onSettled` (return cleanup), not `onCleanup`.
728
887
  - [ ] If the project has a router: `createRouter({ routes })`, not JSX `<Route>` / `<A>`.
888
+ One instance, no nested `<Router>`. Navigate with `useNavigate` / `<a href>`, not
889
+ `window.location`. Router `action`/`query` come from `@solidjs/router` (POST forms + cache), not
890
+ core `action`/`refresh`. Forms: `<form action={save} method="post">`.
891
+ - [ ] `render(() => <App />, root)` — a function, not `render(<App />)`. `hydrate` when
892
+ HTML already exists. Stream async/lazy trees (`await renderToStream(...)`).
729
893
  - [ ] `jsxImportSource` is `@solidjs/web`; Vite plugin is `@solidjs/vite-plugin`.
894
+ No `@solidjs/start`, `vinxi`, `"use client"`, or Next.js imports.
730
895
  - [ ] Single return per component; no early returns on reactive conditions.
731
896
  - [ ] `solid2-kit check` and the project's typecheck pass.
@@ -98,6 +98,7 @@ read these before writing route JSX.
98
98
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/affects.md
99
99
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/on-settled.md
100
100
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/refresh.md
101
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/specialized-reactivity/on-cleanup.md
101
102
 
102
103
  ## Reference: components and JSX (solid-js)
103
104
 
@@ -113,6 +114,7 @@ read these before writing route JSX.
113
114
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/components-context/children.md
114
115
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/components-context/lazy.md
115
116
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/components-context/create-unique-id.md
117
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/manual-hydration/no-hydration.md
116
118
 
117
119
  ## Reference: @solidjs/web
118
120
 
@@ -135,6 +137,7 @@ read these before writing route JSX.
135
137
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/jsx-properties/text-content.md
136
138
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/jsx-properties/inner-html.md
137
139
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions.md
140
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions/get.md
138
141
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response.md
139
142
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/get-request-event.md
140
143
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/redirect.md
@@ -145,6 +148,7 @@ read these before writing route JSX.
145
148
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/router-factory.md
146
149
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/navigation.md
147
150
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/data.md
151
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/filesystem.md
148
152
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/title.md
149
153
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/meta.md
150
154
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/link.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Teach AI coding agents (Cursor, Claude Code) to write correct Solid 2.0 instead of React or Solid 1.x — always-applied rules, an agent skill, and a mechanical pattern gate.",
5
5
  "type": "module",
6
6
  "license": "MIT",