solid2-agent-kit 0.11.1 → 0.12.0

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 | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[files...]`): 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`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor), `<Dynamic>` (use `dynamic()`), `import type { JSX } from "solid-js"` (no `JSX` namespace there — `Element` from `solid-js`, DOM `JSX` types from `@solidjs/web`), two-phase effects whose apply only writes a local signal (use a writable derivation), and core `action(async ...)` with a plain async function instead of a generator (only `yield` re-enters the transaction) |
19
+ | Mechanical gate | `solid2-kit check` | same | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[files...]`): 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`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor), `<Dynamic>` (use `dynamic()`), `import type { JSX } from "solid-js"` (no `JSX` namespace there — `Element` from `solid-js`, DOM `JSX` types from `@solidjs/web`), two-phase effects whose apply only writes a local signal (use a writable derivation), core `action(async ...)` with a plain async function instead of a generator (only `yield` re-enters the transaction), and `setStore(async ...)` on a `createStore` / `createOptimisticStore` setter (the draft is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]`) |
20
20
  | Edit-time hook | `postToolUse` entry in `.cursor/hooks.json` | `PostToolUse` entry in `.claude/settings.json` (matcher includes `Bash`) | Runs the mechanical gate automatically on **every agent file edit — including edits made through shell commands** (sed, heredocs, codemods; source paths mentioned in the command are checked after it ran) — and feeds findings straight back into the conversation (Cursor: `additional_context`; Claude Code: stderr + exit 2), so enforcement does not depend on the agent remembering to run `check`. Wired only when the kit is a local devDependency; opt out with `--no-hooks` |
21
21
  | Turn-end gate | — | `Stop` entry in `.claude/settings.json` | Whole-project `check` + `doctor` when the agent tries to end its turn: with findings in place the stop is blocked (exit 2) and the findings are fed back, so a turn cannot finish with React/Solid 1.x patterns left behind. Loop-safe via `stop_hook_active` (one forced continuation, never an infinite loop) |
22
22
  | Pre-execution guard | `preToolUse` + `beforeShellExecution` entries in `.cursor/hooks.json` | `PreToolUse` entry in `.claude/settings.json` | Denies actions **before they run**: edits/deletes of kit-owned guardrail files (an agent blocked by a gate will try to remove the gate), edits that would strip the kit's hook entries or managed blocks, uninspectable shell rewrites of those files, and `npm/pnpm/yarn/bun install` of banned dependencies (`react`, `vite-plugin-solid`, `eslint-plugin-solid`, SolidStart, ...) — caught proactively instead of post-hoc by `doctor` |
@@ -338,6 +338,24 @@ function soleLocalSetterApply(apply, localSetters) {
338
338
 
339
339
  const LOCAL_SETTER_DECL = /\[\s*[\w$]+\s*,\s*(set[A-Z][\w$]*)\s*\]\s*=\s*createSignal\b/g;
340
340
 
341
+ // A store setter callback is a synchronous transaction — the draft closes when
342
+ // the callback returns, so `setStore(async (draft) => …)` loses every write
343
+ // after its first await (Solid 2.0.0-rc.9 throws ASYNC_STORE_SETTER in dev).
344
+ // Signals may hold promises, so only setters declared via createStore /
345
+ // createOptimisticStore destructuring in the same file are checked.
346
+ const LOCAL_STORE_SETTER_DECL =
347
+ /\[\s*[\w$]+\s*,\s*(set[A-Z][\w$]*)\s*\]\s*=\s*create(?:Optimistic)?Store\b/g;
348
+
349
+ function storeSetterAsyncFindings(content) {
350
+ const setters = new Set([...content.matchAll(LOCAL_STORE_SETTER_DECL)].map((m) => m[1]));
351
+ if (setters.size === 0) return [];
352
+ const findings = [];
353
+ for (const call of content.matchAll(/(?<![.\w$])(set[A-Z][\w$]*)\s*\(\s*async\b/g)) {
354
+ if (setters.has(call[1])) findings.push({ index: call.index });
355
+ }
356
+ return findings;
357
+ }
358
+
341
359
  function effectSyncFindings(content) {
342
360
  const localSetters = new Set([...content.matchAll(LOCAL_SETTER_DECL)].map((m) => m[1]));
343
361
  if (localSetters.size === 0) return [];
@@ -603,7 +621,16 @@ const CHECKS = [
603
621
  id: 'dynamic-jsx',
604
622
  pattern: /<Dynamic[\s/>]/g,
605
623
  message:
606
- '`<Dynamic>` is a JSX convenience wrapper. Application code should use dynamic(() => ...) from "@solidjs/web" so the component identity stays stable.',
624
+ '`<Dynamic>` is deprecated (Solid 2.0.0-rc.9). Hoist a component with dynamic(() => ...) from "@solidjs/web" so the factory is built once and the component identity stays stable.',
625
+ },
626
+ {
627
+ // `setStore(async (draft) => …)` on a setter declared via createStore /
628
+ // createOptimisticStore in this file — the draft closes before the await
629
+ // resumes. `setSignal(async …)` is not flagged (a signal may hold a promise).
630
+ id: 'store-setter-async',
631
+ find: storeSetterAsyncFindings,
632
+ message:
633
+ 'Store setter callbacks are synchronous transactions — writes after the first `await` are lost (dev throws [ASYNC_STORE_SETTER]). Await outside, then call the setter with a synchronous draft mutation (inside an action, `yield` first).',
607
634
  },
608
635
  {
609
636
  // `action(async (item) => ...)` with core `action` never re-enters the
@@ -28,6 +28,11 @@ Steps:
28
28
  `isPending` treated as a global spinner instead of a per-expression question;
29
29
  - treating `<Errored>` as a terminal ErrorBoundary, or routing per-row mutation
30
30
  failures through it (those belong in the action / a projection-folded map);
31
+ error reporting done as a side effect inside `fallback` instead of
32
+ `configureClientErrors` / `configureServerErrors` / `render(..., { onError })`;
33
+ - store setter callbacks that `await` (the draft closes when the callback returns —
34
+ `[ASYNC_STORE_SETTER]`), store setters called at component-body top level, and
35
+ `latest()` used as a null-safe read of an unsettled source;
31
36
  - nested fetches assumed to waterfall, or `<Loading>` lifted along with a lifted fetch;
32
37
  - rewriting App with loading/error branches, or snapshot/restore, when wrapping
33
38
  a client store in server functions; disabling optimistic rows until ack;
@@ -5,4 +5,4 @@
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`. An edit-time hook may also run this gate automatically after each file edit — treat its findings as errors to fix immediately, not warnings. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
6
6
  - After editing `package.json`, `tsconfig*.json`, or root config files (Vite, ESLint), run `solid2-kit doctor` — it fails on React/Solid 1.x wiring: `react`/`vite-plugin-solid`/`eslint-plugin-solid`/SolidStart dependencies, 1.x `solid-js` ranges, and `jsx`/`jsxImportSource` values other than `preserve`/`@solidjs/web`.
7
7
  - The kit's guidance and gate files (`.cursor/rules/solid-2.mdc`, the `solid-2` skill directories, hook entries, managed blocks in this file and `CLAUDE.md`) are guardrails, not obstacles. Never edit, weaken, bypass, or delete them to make a failure go away — fix the flagged code. If a guardrail seems wrong, stop and tell the user.
8
- - 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. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. 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`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`. One browser-only value takes `ssrSource` on the memo/signal/derived store (`client`/`hybrid`), not a `clientOnly` component split. Router link state is automatic (`aria-current`/`data-active`/`data-pending` + CSS; `useLinkState`/`useIsRouting` in JSX); search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Fire-and-forget confirmations wait with `yield until(predicate, {timeout})`. Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, and a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`.
8
+ - 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. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. 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`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`. One browser-only value takes `ssrSource` on the memo/signal/derived store (`client`/`hybrid`), not a `clientOnly` component split. Router link state is automatic (`aria-current`/`data-active`/`data-pending` + CSS; `useLinkState`/`useIsRouting` in JSX); search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Fire-and-forget confirmations wait with `yield until(predicate, {timeout})`. Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write), and a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`. Error monitoring is `configureClientErrors` / `configureServerErrors` (or `render(..., { onError })`), not a side effect inside `<Errored fallback>`. `<Dynamic>` is deprecated — use `dynamic()`.
@@ -15,7 +15,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
15
15
  1. **Never destructure props.** `function C({ name })` and `const { name } = props` read the
16
16
  prop getter once at setup and kill reactivity. Keep `props.x` and read it inside JSX, a
17
17
  memo, or an effect compute function. Defaults go at the read site: `props.x ?? fallback`.
18
- Rest props: `omit(props, "label", "value")`, never `const rest = { ...props }` (a snapshot).
18
+ Rest props: `omit(props, "label", "value")` (or a predicate: `omit(props, (k) => k.startsWith("data-"))`),
19
+ never `const rest = { ...props }` (a snapshot). `merge` / `omit` return read-only live
20
+ views — assigning onto them is a silent no-op; `{ ...merged }` gives an own object that
21
+ never updates again.
19
22
  JSX `{...rest}` is fine once `rest` is a reactive proxy. Rule of thumb: the string `props.`
20
23
  must never appear at component-body top level *as a read* (`const x = props.x`); nested
21
24
  functions (`() => props.x`, `omit(props, ...)`, `children(() => props.children)`) are fine.
@@ -83,6 +86,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
83
86
  `reconcile` or the derived form (dev + attribution flags the store-level waste as
84
87
  `[IMMUTABLE_UPDATE_IN_STORE]`). Stores are
85
88
  **property reads** (`todos.length`, `todo.title`), not accessors — never `todos()`.
89
+ The setter callback is a **synchronous** transaction: `setStore(async (draft) => …)`
90
+ throws `[ASYNC_STORE_SETTER]` in dev (writes after the first `await` were silently
91
+ lost before). `await` first, then call the setter (inside an `action`, `yield` first).
92
+ Mechanically enforced by `solid2-kit check`. A store setter called at
93
+ component-body top level is a write in an owned scope (`[REACTIVE_WRITE_IN_OWNED_SCOPE]`,
94
+ same as a signal setter) — seed the initial shape through the `createStore` argument.
86
95
  10. **Async data is an async computation**: `createMemo(async () => ...)` (or
87
96
  `createMemo(() => fetchUser(id()))`) read under `<Loading>` / `<Errored>`.
88
97
  **Fetch high, block low** — creating/reading are like sync; consuming (await)
@@ -103,7 +112,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
103
112
  accessor) only when that identity change should show the fallback again. Do not start `fetch` (or any request) at component-body top
104
113
  level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
105
114
  use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
106
- `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer.
115
+ `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer, and not a
116
+ null-safe probe: `latest(user)` before the first value throws `NotReadyError` in every
117
+ scope (event handlers included) — it never returns `undefined` for an unsettled source.
107
118
  Default navigation holds: keep `selectedId()` so highlight and content stay
108
119
  consistent; `latest(selectedId)` only when the highlight should move first.
109
120
  Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
@@ -176,6 +187,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
176
187
  live reconnect, input change). `reset` retries the collected *sources*, not a UI
177
188
  remount. Per-row mutation failures belong in an errors map the projection folds
178
189
  in (survives the optimistic overlay), not in `<Errored>` around the whole list.
190
+ Error monitoring is a hook, not a side effect in the fallback: the boundary-caught
191
+ failures no global handler sees reach `configureClientErrors({ onError })` from
192
+ `solid-js` (or `render(..., { onError })` per root) and `configureServerErrors({ onError })`
193
+ from `@solidjs/web` — do not call `captureException` inside `<Errored fallback>`.
179
194
  Making a client store "real" is additive: same setters, wrap mutations in
180
195
  `action`, swap `createOptimisticStore` and a file of server functions — do
181
196
  not rewrite `App.tsx` with loading/error branches.
@@ -212,7 +227,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
212
227
  cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
213
228
  the sync mutation *is* the prediction (an overlay, discarded on settle); write the
214
229
  body as a generator (`function*` / `async function*`) suspending on `yield`, never a
215
- plain `async` function (`solid2-kit check` flags that); put
230
+ plain `async` function (`solid2-kit check` flags that). After a bare `await`, put a
231
+ bare `yield` before the next write **and** before anything that creates a reader
232
+ (`yield until(...)`, `latest()`, a memo/effect) — `until(...)`'s argument is evaluated
233
+ in the post-`await` continuation, outside the transaction; put
216
234
  `pending` on the record rather than a second copy of state. Do not snapshot
217
235
  and restore, disable optimistic rows until ack, mutex rapid clicks, or freeze
218
236
  unrelated writes because one action is in flight; failed-action replay is
@@ -263,7 +281,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
263
281
  — `lazy(() => import("./pages").then((m) => ({ default: m.About })))` hydrates wrong
264
282
  because the named export is not a call-site literal. Select a component from reactive
265
283
  state with `dynamic(() => ...)` from `@solidjs/web`
266
- (stable identity). If the project has `@solidjs/router`, routes are `createRouter({ routes })`
284
+ (stable identity); `<Dynamic component={...}>` is deprecated (`@deprecated` in the
285
+ types since 2.0.0-rc.9, still shipped in 2.0). If the project has `@solidjs/router`, routes are `createRouter({ routes })`
267
286
  at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
268
287
  `query` are URL-addressable POST forms and a read cache — not core `action` /
269
288
  `refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
@@ -291,7 +310,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
291
310
  (`<meta name="description">`) that Solid Meta should manage.
292
311
  Return `respond(value, { status, headers })` from a `"use server"` function, not
293
312
  `Response.json` — a raw `Response` is HTTP-handler control flow; scripted
294
- callers should receive the value. Reads are function calls too (`await getUser(id)`
313
+ callers should receive the value. Production sanitizes every thrown value that
314
+ crosses the wire — server-function throws **and** SSR render errors (an `<Errored>`
315
+ fallback printing `err().message` shows `"Internal Server Error"` in a production
316
+ server render) — so intentional client-facing failures are `markSafeError(...)` or
317
+ `throw respond(body, { status })`. Reads are function calls too (`await getUser(id)`
295
318
  on a `GET()`-declared read; the method mirrors the operation). Return or throw a
296
319
  redirect (`if (!session?.userId) throw redirect("/sign-in")`); `redirect()` /
297
320
  `respond()` take `revalidate` like `reload()`. Cache GET reads with
@@ -367,7 +390,7 @@ directory (default `src/`).
367
390
  | `startTransition`, `useTransition` | automatic held updates + `isPending` |
368
391
  | `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
369
392
  | `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
370
- | `createDynamic(...)` | `dynamic(source)` from `@solidjs/web` |
393
+ | `createDynamic(...)`, `<Dynamic component={...}>` (deprecated in 2.0) | `dynamic(source)` from `@solidjs/web` |
371
394
  | `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
372
395
  | `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
373
396
  | `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
@@ -383,7 +406,8 @@ When unsure about any API, verify against the official Solid 2.0 docs — fetcha
383
406
  listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
384
407
  Solid 1.x or React memory.
385
408
 
386
- `createTrackedEffect`, `createRenderEffect`, and `storePath` exist in Solid 2 but
387
- are not defaults: use two-phase `createEffect` unless a single tracked callback is
388
- required, and draft setters instead of `storePath` except while converting 1.x
389
- path setters.
409
+ `createTrackedEffect` and `createRenderEffect` exist in Solid 2 but are not defaults:
410
+ use two-phase `createEffect` unless a single tracked callback is required. `storePath`
411
+ (the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
412
+ 2.0.0-rc.9 — convert path setters to draft setters instead of importing it from
413
+ `@solidjs/signals`.
@@ -68,7 +68,7 @@ Most React `useEffect` code should NOT become `createEffect`:
68
68
  | Inspect, count, or iterate children | `children(() => props.children)` then `.toArray()` |
69
69
  | Code-split a component | `lazy(() => import("./X"))` read under `<Loading>` |
70
70
  | Named export from a lazy module | `lazy(() => import("./pages"), { export: "About" })` |
71
- | Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; prefer over `<Dynamic>`) |
71
+ | Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; `<Dynamic>` is deprecated) |
72
72
  | Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
73
73
  | Async value used several layers down | Create the memo high; pass `value={memo()}` through intermediates (they do not wait); put `<Loading>` around the leaf read |
74
74
  | Nested child with its own fetch | Leave it nested — it runs in parallel. Sequential only when the second call needs the first response (`fetchAuthor(story().authorId)`) |
@@ -120,7 +120,11 @@ To merge defaults or split props reactively use `merge` / `omit` from `solid-js`
120
120
  Omitted keys still fall through, so `merge({ type: "button" }, props)` is the
121
121
  usual defaults pattern. That is not Solid 1 `mergeProps`, which ignored
122
122
  `undefined`. Forwarding leftover props: `const rest = omit(props, "label")` then
123
- `<input {...rest} />`. `const rest = { ...props }` compiles and then never updates.
123
+ `<input {...rest} />`; a predicate hides keys by rule without enumerating first
124
+ (`omit(props, (key) => key.startsWith("data-"))`). `const rest = { ...props }` compiles
125
+ and then never updates. Both helpers return **read-only live views** (since
126
+ 2.0.0-rc.9 for every input, plain objects included): assigning onto the result is a
127
+ silent no-op, and `{ ...merged }` is an own object that carries no sources.
124
128
 
125
129
  ### Store updates (draft mutation)
126
130
 
@@ -134,6 +138,26 @@ setProfile((draft) => {
134
138
  });
135
139
  ```
136
140
 
141
+ The setter callback is a **synchronous transaction** — the draft closes when it
142
+ returns. `setProfile(async (draft) => { draft.name = await load(); })` used to commit
143
+ only the writes before the first `await` and silently drop the rest; since
144
+ 2.0.0-rc.9 it throws `[ASYNC_STORE_SETTER]` in dev (`solid2-kit check` flags it too).
145
+ Await outside, then write synchronously (inside an `action`, `yield` first):
146
+
147
+ ```tsx
148
+ // WRONG — the draft is already closed when the await resumes
149
+ setProfile(async (draft) => { draft.name = await fetchName(); });
150
+
151
+ // CORRECT
152
+ const name = await fetchName();
153
+ setProfile((draft) => { draft.name = name; });
154
+ ```
155
+
156
+ A signal may hold a promise, so `setSignal` has no such rule. A store setter called
157
+ at component-body top level is a write in an owned scope — dev throws
158
+ `[REACTIVE_WRITE_IN_OWNED_SCOPE]` (rc.9 closed the exemption store writes had) — so
159
+ seed the shape through the `createStore` argument or a derivation.
160
+
137
161
  **External collections enter stores through reconciliation, never wholesale assignment.**
138
162
  `draft.todos = serverTodos` renders correctly but replaces every object identity — all
139
163
  subscribers under the path re-run and row identity is lost. In order of preference:
@@ -204,8 +228,11 @@ const Result = dynamic(() => (detailed() ? Detailed : Compact));
204
228
  `dynamic()` returns a **stable** component whose source can be a component, an
205
229
  intrinsic tag name, a promise, or empty. Prefer it over swapping a component
206
230
  variable in JSX (`const View = tab() ? A : B` freezes the choice at setup).
207
- `<Dynamic>` is the JSX spelling of the same primitive; application code should
208
- use `dynamic()` so the component identity stays stable.
231
+ `<Dynamic component={...}>` is the same primitive with a worse shape (the tag travels
232
+ in the props bag, so every instance merges and omits it and rebuilds a factory) and
233
+ is **deprecated** since 2.0.0-rc.9 — `@deprecated` in the types, no runtime warning,
234
+ still shipped in 2.0. Hoist a `dynamic()` per component instance (or per module for a
235
+ constant tag) instead.
209
236
 
210
237
  ### Two-phase effect (imperative boundary only)
211
238
 
@@ -357,7 +384,11 @@ return <StoryDetail story={story} />;
357
384
  React ErrorBoundary. `reset` retries the collected *data sources*, not a UI remount.
358
385
  - Refetch: `refresh(results)`. In-flight indicator: `isPending(results)` (or
359
386
  `isPending(() => results())`). Freshest in-flight value for a preview:
360
- `latest(results)`. Do not start `fetch` at component-body top level.
387
+ `latest(results)`. `latest` is not a null-safe probe: before the source's first value
388
+ it throws `NotReadyError` in every scope (since 2.0.0-rc.9 also in event handlers and
389
+ imperative code, which used to receive `undefined`). A write is visible to `latest()` /
390
+ `isPending()` only from the flush that carries it — `flush()` first when a test reads
391
+ its own write through them. Do not start `fetch` at component-body top level.
361
392
  - Coordinate sibling `<Loading>` reveal with `<Reveal>` (`order="sequential"` default,
362
393
  or `"together"` / `"natural"`). `collapsed` (sequential only) suppresses tail
363
394
  skeletons past the frontier so the page does not stack fallbacks as popcorn.
@@ -384,6 +415,32 @@ repeating `String(error())` at each boundary:
384
415
  <Errored fallback={(error, reset) => <ErrorFallback error={error} reset={reset} />}>
385
416
  ```
386
417
 
418
+ **Report boundary-caught errors through the error hooks, not from the fallback.** An
419
+ error an `<Errored>` collected never reaches `window.onerror`; a `captureException`
420
+ call inside `fallback` is a side effect in render that fires on every re-render of the
421
+ fallback. Since 2.0.0-rc.9 the runtime has a seam for exactly this failure, called
422
+ once per error object with where it was thrown (`ownerPath`) and where it was met
423
+ (`boundaryPath`):
424
+
425
+ ```ts
426
+ import { configureClientErrors } from 'solid-js';
427
+ import { configureServerErrors } from '@solidjs/web';
428
+
429
+ configureClientErrors({
430
+ onError: (error, { ownerPath, boundaryPath }) => report(error, { ownerPath, boundaryPath }),
431
+ });
432
+ // server side (SSR renders and server functions); its return value, if any, is the
433
+ // sanitized value the client receives instead of the original error
434
+ configureServerErrors({ onError: (error, { kind, handling }) => report(error, { kind, handling }) });
435
+ ```
436
+
437
+ `render` / `hydrate` accept a per-root `onError` that wins over the ambient hook.
438
+ Uncaught errors (`REACTIVITY_HALTED`) go to the platform's `reportError`, which every
439
+ monitor already listens on — do not report them twice. `renderToStream` /
440
+ `renderToString`'s `onError` option *is* the per-render server hook: it now hears every
441
+ handled failure, so filter on `context.handling === "failed"` when only request-failing
442
+ errors matter (the rc.8 one-argument form keeps working).
443
+
387
444
  ### Streams and subscriptions (websockets, reactive clients)
388
445
 
389
446
  Push-based sources integrate by returning an **async iterable** from a computation — reads
@@ -532,7 +589,11 @@ Write the body as a generator — `function*` or `async function*`, never a plai
532
589
  `async (...) =>` — because only `yield` re-enters the transaction: the runtime has no
533
590
  hook into a plain `await` continuation, so even inside `async function*` an `await`
534
591
  leaves the transaction until the next `yield` (`solid2-kit check` flags the
535
- non-generator form).
592
+ non-generator form). The bare `yield` must also come before anything that **creates a
593
+ reader** — `until()`, `latest()`, a memo or effect, a mount — not only before writes:
594
+ the expression of the next `yield` is evaluated in the post-`await` continuation, so
595
+ `await save(); yield until(...)` builds the predicate's reader outside the transaction.
596
+ Write `await save(); yield; yield until(...)`.
536
597
 
537
598
  When confirmation arrives outside the response — a fire-and-forget transport echoed
538
599
  back on a live source — hold the action open with `yield until(predicate, { timeout })`:
@@ -792,10 +853,18 @@ an agent sneaks in fails the suite instead of scrolling by.
792
853
  Diagnostics come in two layers, both dev-build only (production strips them and `DEV`
793
854
  is `undefined` there). Always-on findings include misplaced reads
794
855
  (`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`), misplaced writes
795
- (`[REACTIVE_WRITE_IN_OWNED_SCOPE]`, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`), and
856
+ (`[REACTIVE_WRITE_IN_OWNED_SCOPE]` — since rc.9 also for store setters called in a
857
+ component or root body, `[ASYNC_STORE_SETTER]` for a store setter callback that returns
858
+ a promise, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`), and
796
859
  `[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it.
797
860
  Each entry carries a stable `[CODE]`, what the runtime observed, and an owner chain
798
861
  (`in <App> › <Cart> › <LineItem>`); never silence a code before understanding it.
862
+ Since rc.9 the server render reports on the same channel with the same owner chains:
863
+ `[SSR_RENDER_ERROR_CONTAINED]` (a boundary routed a render error; `ownerPath` is where
864
+ it was thrown, `boundaryPath` where it was met), `[SSR_ERROR_SANITIZED]` (the original
865
+ behind a production-sanitized error), `[SSR_CLIENT_CONTENT_MASKED]` (client-only content
866
+ that surfaced only after a real server wait — the wait was wasted), server-side
867
+ `[ASYNC_WATERFALL]`, and `[REVEAL_IN_RENDER_TO_STRING]`.
799
868
  For the repair behind any code, read the `reactivity-diagnostics` skill Solid ships
800
869
  in the repo (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) — every
801
870
  code maps to what the runtime observed and what to change.
@@ -805,16 +874,20 @@ every scope that re-ran, what changed to cause it, and how long it took. Since
805
874
  `solid-js` 2.0.0-rc.8 the engine lives behind its own subpath — `DEV.attribution`
806
875
  no longer exists (it is a type error; `DEV` holds only devtools hooks, graph
807
876
  traversal, and console reporting), and the low-level hook slot on `OBSERVE` is for
808
- engines and devtools, not app code:
877
+ engines and devtools, not app code. Since 2.0.0-rc.9 the folds, queries, and
878
+ formatters are **named exports** of that subpath, not methods of `attribution`
879
+ (`attribution.why` / `.costs` / `.feedback` / `.subscriptions` / `.format` are type
880
+ errors); `attribution` keeps `enable`, `disable`, `subscribe`, `markFlight`, and the
881
+ record ring buffers (`history`, `holds`, `waterfalls`, `navigations`, `interactions`):
809
882
 
810
883
  ```ts
811
- import { attribution } from 'solid-js/attribution';
884
+ import { attribution, formatRerun, why } from 'solid-js/attribution';
812
885
  import { isDev } from '@solidjs/web';
813
886
 
814
887
  if (isDev) attribution.enable({ log: false });
815
888
  // reproduce the interaction, then ask why a scope ran:
816
- for (const event of attribution.why(total)) {
817
- console.log(attribution.format(event));
889
+ for (const event of why(total)) {
890
+ console.log(formatRerun(event));
818
891
  }
819
892
  ```
820
893
 
@@ -823,8 +896,10 @@ for (const event of attribution.why(total)) {
823
896
  to query the tables yourself. Name the scopes you intend to interrogate
824
897
  (`createMemo(..., { name: "total" })`) — chains refer to nodes by name, and
825
898
  anonymous nodes print as `computed` / `effect` / `signal`. In production the import
826
- resolves to an inert same-surface twin (~640 bytes) whose `enable()` is a no-op, so
827
- with the `isDev` guard the cost is zero and the import can stay in the code.
899
+ resolves to an inert same-surface twin whose `enable()` is a no-op, so
900
+ with the `isDev` guard the cost is zero and the import can stay in the code. The
901
+ split is pay-for-use: importing `costs` or `feedback` is what turns their tables on,
902
+ so a records-only consumer (`attribution.subscribe(...)`) ships neither.
828
903
 
829
904
  While attribution is enabled, Solid also warns on its own about over-subscription,
830
905
  chained async waterfalls (`[ASYNC_WATERFALL]`), memos whose fresh-but-equal output
@@ -835,10 +910,11 @@ row-rebuilding lists (`[UNSTABLE_LIST_IDENTITY]` — key by id or reconcile into
835
910
  store), and silent holds (`[SILENT_HOLD]` — a write held on async work with no
836
911
  `isPending()` / `latest()` reader, optimistic value, `affects()` mark, or effect that
837
912
  ran while held; info from 100ms, warning from 200ms, tunable via
838
- `enable({ holds: { infoMs, warnMs } })`). `attribution.costs()`, `holds()`, and
839
- `feedback()` rank the session worst-first; `waterfalls()`, `navigations()`, and
840
- `interactions()` hold the fact tables behind the verdicts.
841
- `attribution.subscriptions(scope)` lists a scope's current dependencies to compare
913
+ `enable({ holds: { infoMs, warnMs } })`). `costs()`, `feedback()` (named imports), and
914
+ `attribution.holds()` rank the session worst-first; `attribution.waterfalls()`,
915
+ `attribution.navigations()`, and `attribution.interactions()` hold the fact tables
916
+ behind the verdicts.
917
+ `subscriptions(scope)` (named import) lists a scope's current dependencies to compare
842
918
  against what it uses.
843
919
 
844
920
  For regression tests, `@solidjs/diagnostics` turns the same channels into assertions:
@@ -1118,11 +1194,17 @@ reaches the client — the directive is the privacy boundary. Mutations that sho
1118
1194
  revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
1119
1195
  guides). Return or throw a redirect — guards read `if (!session?.userId) throw
1120
1196
  redirect("/sign-in")`. `redirect()` and `respond()` accept the same `revalidate`
1121
- option as `reload()` when the mutation also changes location or returns a value. After a router/server mutation do **not** follow with a client `fetch`
1197
+ option as `reload()` when the mutation also changes location or returns a value;
1198
+ `revalidate: REVALIDATE_ALL` (`"*"`, from `@solidjs/web`) declares every cache entry
1199
+ stale, distinct from omitting it (the host's default) and from `[]` (nothing). After a router/server mutation do **not** follow with a client `fetch`
1122
1200
  to "refresh the page" — `reload` / `revalidate` (and the router's single-flight
1123
1201
  response) close that loop. For 404 during SSR: `httpStatus(404)` from `@solidjs/web`.
1124
1202
  `throw new Error("…")` is stripped to `"Internal Server Error"` in production —
1125
- use `markSafeError` or `respond(..., { status })` for intentional client-facing
1203
+ on the server-function wire **and**, since 2.0.0-rc.9, on every SSR road a failure
1204
+ takes (an `<Errored>` fallback rendered on the server, a rejected async source
1205
+ serialized into the stream, a `<Loading>` fragment's rejection), so `err().message`
1206
+ in a fallback reads the generic message in a production server render. Use
1207
+ `markSafeError` or `respond(..., { status })` for intentional client-facing
1126
1208
  failures. Do not `return Response.json(...)` from `"use server"` — that is
1127
1209
  HTTP-handler control flow; `respond(value, { status })` is what a scripted
1128
1210
  caller unwraps. JSON-encodable arguments only unless `enableRichArguments()`
@@ -1317,7 +1399,14 @@ how to reuse. Prefer the form on the right.
1317
1399
  | `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
1318
1400
  | `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
1319
1401
  | `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
1320
- | `<Dynamic component={tab() ? A : B} />` | `const View = dynamic(() => (tab() ? A : B))` — `<Dynamic>` is a JSX convenience; app code should use `dynamic()` for stable identity |
1402
+ | `<Dynamic component={tab() ? A : B} />` | `const View = dynamic(() => (tab() ? A : B))` — `<Dynamic>` is deprecated (rc.9); `dynamic()` hoists the factory and keeps identity stable |
1403
+ | `setStore(async (draft) => { draft.x = await load(); })` | `const x = await load(); setStore((draft) => { draft.x = x; })` — the setter is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]` |
1404
+ | `setStore((d) => { d.items = seed; })` in a component body | seed through `createStore(seed)` / `createStore(() => props.items, fallback)` — a body-level store write is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` |
1405
+ | `const saved = await api.save(); yield until(() => ...)` | `await api.save(); yield; yield until(() => ...)` — a bare `yield` re-enters the transaction before any reader is created, not only before writes |
1406
+ | `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or `render(..., { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error, with `ownerPath` / `boundaryPath` |
1407
+ | `latest(user) ?? placeholder` in a handler as a null-safe read | `latest()` throws `NotReadyError` before the first value in every scope; read settled values under `<Loading>`, or `await until(() => user())` |
1408
+ | `attribution.why(total)` / `attribution.format(e)` / `attribution.costs()` | named imports from `solid-js/attribution`: `why(total)`, `formatRerun(e)`, `costs()`, `feedback()`, `subscriptions(scope)` (rc.9) |
1409
+ | `import { storePath } from "solid-js"` | gone from `solid-js` (rc.9); write draft setters `setState((d) => { d.user.name = "Grace"; })` |
1321
1410
  | `<meta name="description" content="...">` hardcoded in the document shell | `<Meta>` from `@solidjs/meta`. A static `<title>` is the fallback; other hardcoded tags coexist with the registry |
1322
1411
  | `useHead({ tag: "meta", ... })` for ordinary page tags | `<Title>` / `<Meta>` / `<Script>` (JSON-LD). `useHead` is the lower-level registry |
1323
1412
  | two `<Meta property="og:image">` meant as one replacement set | wrap them in `<Head>` — a later group replaces the earlier set as one unit |
@@ -1490,7 +1579,9 @@ always-applied rules installed alongside this skill.
1490
1579
  or `isPending(selectedId)` when the write is the question. `<Errored>` heals
1491
1580
  when the source succeeds; `reset` retries sources, not a UI remount. Per-row
1492
1581
  mutation failures stay in the action/projection, not in `<Errored>` around
1493
- the list. Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
1582
+ the list. Error monitoring goes through `configureClientErrors` /
1583
+ `configureServerErrors` (or `render(..., { onError })`), never a side effect in
1584
+ the fallback. Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
1494
1585
  different APIs — do not mix them. Client → server is additive (same
1495
1586
  setters + `action` + server functions): do not rewrite App with
1496
1587
  loading/error branches, disable optimistic rows until ack, or refetch in
@@ -1513,11 +1604,14 @@ always-applied rules installed alongside this skill.
1513
1604
  (values), not `typeof window`.
1514
1605
  Component teardown is `onSettled` (return cleanup), not `onCleanup`.
1515
1606
  - [ ] Dev console shows no diagnostics: reads in tracking scopes (no
1516
- `[STRICT_READ_UNTRACKED]`), held writes paired with `isPending` / `latest` /
1517
- optimistic values / `affects()` (no `[SILENT_HOLD]`). Attribution-only costs
1607
+ `[STRICT_READ_UNTRACKED]`), store setters synchronous and never called at
1608
+ body level (no `[ASYNC_STORE_SETTER]` / `[REACTIVE_WRITE_IN_OWNED_SCOPE]`), held
1609
+ writes paired with `isPending` / `latest` / optimistic values / `affects()`
1610
+ (no `[SILENT_HOLD]`). Attribution-only costs
1518
1611
  (`[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`, `[EFFECT_*]`)
1519
- checked via `attribution` from `solid-js/attribution` when touching stores,
1520
- lists, or effects. Name interrogated scopes (`{ name: "total" }`).
1612
+ checked via `attribution` + the named folds (`why`, `costs`, `feedback`) from
1613
+ `solid-js/attribution` when touching stores, lists, or effects. Name
1614
+ interrogated scopes (`{ name: "total" }`).
1521
1615
  - [ ] If the project has a router: `createRouter({ routes })`, not JSX `<Route>` / `<A>`.
1522
1616
  One instance, no nested `<Router>`. Navigate with `useNavigate` / `<a href>`, not
1523
1617
  `window.location`. Router `action`/`query` come from `@solidjs/router` (POST forms + cache), not
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
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",