solid2-agent-kit 0.9.0 → 0.11.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`), and two-phase effects whose apply only writes a local signal (use a writable derivation) |
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) |
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` |
@@ -127,8 +127,8 @@ Run both gates on every PR so nothing an agent (or human) merges can reintroduce
127
127
  Solid 1.x patterns:
128
128
 
129
129
  ```yaml
130
- - uses: actions/setup-node@v4
131
- with: { node-version: 22 }
130
+ - uses: actions/setup-node@v7
131
+ with: { node-version: 26 }
132
132
  - run: npm ci
133
133
  - run: npx solid2-agent-kit check && npx solid2-agent-kit doctor
134
134
  ```
@@ -354,6 +354,18 @@ function effectSyncFindings(content) {
354
354
  return matches;
355
355
  }
356
356
 
357
+ // Core `action` (solid-js) takes a generator (`function*` / `async function*`)
358
+ // so `yield` can re-enter the transaction; router `action` (@solidjs/router)
359
+ // legitimately takes `async (form) => ...`. Only flag `action(async` when this
360
+ // file's `action` comes from "solid-js" — a router import (or none) stays silent.
361
+ function actionAsyncFindings(content) {
362
+ if (!/import\s*\{[^}]*\baction\b[^}]*\}\s*from\s*['"]solid-js['"]/.test(content)) return [];
363
+ if (/import\s*\{[^}]*\baction\b[^}]*\}\s*from\s*['"]@solidjs\/router['"]/.test(content)) return [];
364
+ return [...content.matchAll(/\baction\s*\(\s*async(?!\s+function\s*\*)/g)].map((match) => ({
365
+ index: match.index,
366
+ }));
367
+ }
368
+
357
369
  const CHECKS = [
358
370
  {
359
371
  id: 'props-destructure-param',
@@ -511,7 +523,7 @@ const CHECKS = [
511
523
  id: 'solid1-router',
512
524
  pattern: /<(?:HashRouter|MemoryRouter|Route|Navigate|A|FileRoutes|StartClient|StartServer)\b/g,
513
525
  message:
514
- 'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}>.',
526
+ 'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}> (link state: automatic aria-current/data-active/data-pending + CSS, or useLinkState).',
515
527
  },
516
528
  {
517
529
  id: 'meta-provider',
@@ -593,6 +605,14 @@ const CHECKS = [
593
605
  message:
594
606
  '`<Dynamic>` is a JSX convenience wrapper. Application code should use dynamic(() => ...) from "@solidjs/web" so the component identity stays stable.',
595
607
  },
608
+ {
609
+ // `action(async (item) => ...)` with core `action` never re-enters the
610
+ // transaction — `action(async function* ...)` does not match.
611
+ id: 'action-plain-async',
612
+ find: actionAsyncFindings,
613
+ message:
614
+ 'Core `action` takes a generator (`function*` / `async function*`), not a plain async function — only `yield` re-enters the transaction, so writes after a bare `await` commit immediately. (Router `action` from "@solidjs/router" is the one that takes `async (form) => ...`.)',
615
+ },
596
616
  ];
597
617
 
598
618
  // .ts / .tsx / .jsx sources, excluding .d.ts declaration files.
@@ -17,6 +17,9 @@ Steps:
17
17
  - async computations reading reactive inputs **after** the first `await`;
18
18
  - effects that copy state instead of deriving it, or reading stores in the apply phase;
19
19
  - `<For>` over server/refetched rows without a stable-id key function;
20
+ - core `action` bodies that are not generators (plain `async` functions) or that write
21
+ after a bare `await` without re-entering on `yield` (router `action` from
22
+ `@solidjs/router` is the one that takes `async (form) => ...`);
20
23
  - `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
21
24
  `const u = user()` extracted then passed (a real parent-side read). Passing
22
25
  `user={user()}` is the colorless form — do not "fix" it into accessors or
@@ -31,7 +34,22 @@ Steps:
31
34
  - tRPC / type-gen around `"use server"`, a client `fetch` after a mutation,
32
35
  refetch in `hydrate`/`onSettled`, a custom Worker/Express adapter, or
33
36
  delaying `renderToStream` for visual order (`<Reveal>` owns display);
34
- - store updates that rebuild objects/arrays instead of mutating the draft or reconciling;
37
+ - store updates that rebuild objects/arrays instead of mutating the draft or reconciling
38
+ (`filter` for removal is fine — survivors keep identity; only fresh-tree wholesale
39
+ replacement needs `reconcile` / the derived form);
40
+ - hand-rolled link-active comparisons (`location.pathname === ...`) instead of the
41
+ automatic `aria-current` / `data-active` / `data-pending` attributes (+ CSS) or
42
+ `useLinkState` / `useIsRouting`; hand-rolled query parsing instead of
43
+ `useSearchParams`;
44
+ - search inputs bound to a held value without `latest`, effect-based debounce, fully
45
+ controlled forms without `name`s, `{ success: false }` result objects instead of
46
+ thrown failures, or router forms without server-side validation (`throw respond(...)`)
47
+ and `useSubmissions` inline errors;
48
+ - browser-only *values* handled with a `clientOnly` component split or an `isServer`
49
+ branch instead of `ssrSource: "client"` / `"hybrid"` on the memo/signal/derived store;
50
+ - held writes with no feedback (`isPending` / `latest` / optimistic value / `affects()`)
51
+ — the `[SILENT_HOLD]` shape — and dev-console diagnostics (`[STRICT_READ_UNTRACKED]`,
52
+ attribution-only store/list/effect costs) left unaddressed;
35
53
  - context values passed as snapshots instead of accessors/setters/stores;
36
54
  - components with conditional/early returns on reactive values.
37
55
  5. Verify any API you are not certain about against the official docs mirror
@@ -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`.
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]`.
@@ -21,7 +21,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
21
21
  functions (`() => props.x`, `omit(props, ...)`, `children(() => props.children)`) are fine.
22
22
  Mechanically enforced by `solid2-kit check` — run it after editing TSX.
23
23
  2. **Never read reactive values at component-body top level.** The body runs once, untracked;
24
- `const v = count()` there is a frozen snapshot (Solid warns in dev). Carry the accessor
24
+ `const v = count()` there is a frozen snapshot (Solid reports `[STRICT_READ_UNTRACKED]`
25
+ in dev, naming the component). Carry the accessor
25
26
  (the function itself) into JSX and call it there.
26
27
  3. Signals are read by **calling** them: `{count()}` in JSX, not `{count}`.
27
28
  4. **Derived state is a function, not state + effect.** `const full = () => a() + b()`.
@@ -30,6 +31,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
30
31
  `createSignal(() => { source(); return initial; })` (re-runs when `source()` changes;
31
32
  the setter still works) — never a `createEffect` whose apply calls the setter. The
32
33
  derivation also moves the `source()` read to the read site, under its boundaries.
34
+ (React needs `useState(props.x)` plus a `previousX` comparison reset for the same
35
+ derive-override-reset behavior.)
33
36
  Mechanically enforced by `solid2-kit check` (effects whose apply only writes a local
34
37
  signal). Use `createMemo` only for expensive
35
38
  computations, multiple consumers, or an equality boundary. When several writes can land
@@ -52,13 +55,15 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
52
55
  `event.currentTarget` (the element that owns the handler), not `event.target`
53
56
  (a nested child). Never pass a setter as the handler (`onClick={setCount}`) —
54
57
  that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
58
+ Debounce at the handler (`onInput={debounce(...)}`), never as an effect copying
59
+ one signal into another.
55
60
  7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props.
56
61
  `{todos().map((t) => <Row todo={t} />)}` *renders*, then recreates every row on each
57
62
  update. Row identity: default = item reference; `keyed={(item) => item.id}` = key function
58
63
  (child receives item as an accessor); `keyed={false}` = positional. **Rows from
59
64
  server/refetched data (fresh object references on every update — fetch results,
60
65
  subscription payloads) must use a key function on a stable id**, or every update
61
- recreates every row. Reference keying is for local arrays whose item identities are
66
+ recreates every row (dev + attribution flags the rebuilds as `[UNSTABLE_LIST_IDENTITY]`). Reference keying is for local arrays whose item identities are
62
67
  stable (e.g. store rows). Match the child signature to the mode: default item is the
63
68
  **raw** value (`todo.title`); `keyed={(t) => t.id}` / `keyed={false}` pass an
64
69
  **accessor** (`todo().title`). Mixing those still “runs” (you render a function, or
@@ -69,11 +74,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
69
74
  `compute`; its return value feeds `apply`, which does imperative work and may return a
70
75
  cleanup. Single-argument `createEffect(fn)` is an error in Solid 2. Do not substitute
71
76
  `createTrackedEffect` for that — it is an advanced one-callback form that cannot nest
72
- primitives. Most React `useEffect` code should not become an effect at all — see the skill.
77
+ primitives. Skip the initial run with `{ defer: true }`. Most React `useEffect` code should not become an effect at all — see the skill.
73
78
  9. **Stores update by mutating a draft**: `setStore(draft => { draft.user.name = "Ada" })`.
74
- Never rebuild with spreads — that destroys property-level subscriptions. Returning a new
75
- array from the setter (`setTodos((t) => t.filter(...))`) *renders* but is unkeyed — same
76
- identity loss as wholesale assignment; mutate the draft or `reconcile`. Stores are
79
+ Never rebuild with spreads — that destroys property-level subscriptions. The return
80
+ form is for shapes where mutation is awkward — most commonly removal
81
+ (`setTodos((t) => t.filter(...))` is fine; survivors keep identity). Replacing an
82
+ array with a fresh tree replaces rows by index — that is the identity loss; use
83
+ `reconcile` or the derived form (dev + attribution flags the store-level waste as
84
+ `[IMMUTABLE_UPDATE_IN_STORE]`). Stores are
77
85
  **property reads** (`todos.length`, `todo.title`), not accessors — never `todos()`.
78
86
  10. **Async data is an async computation**: `createMemo(async () => ...)` (or
79
87
  `createMemo(() => fetchUser(id()))`) read under `<Loading>` / `<Errored>`.
@@ -99,13 +107,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
99
107
  Default navigation holds: keep `selectedId()` so highlight and content stay
100
108
  consistent; `latest(selectedId)` only when the highlight should move first.
101
109
  Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
102
- `transition-delay` so fast swaps do not flash.
110
+ `transition-delay` so fast swaps do not flash. Inputs feeding a request bind
111
+ `value={latest(query)}` so keystrokes show while the write is held.
103
112
  `isPending` is a per-expression question, not a global spinner: ask it of the
104
113
  async source, a derived prop (`isPending(() => props.story)`), or the write
105
114
  (`isPending(selectedId)`), even when the fetch lives in a child. It is
106
115
  the *refetch* indicator after a settled answer exists — do not use it as the
107
116
  first-load spinner (`<Loading>` owns that). Pass the accessor: `isPending(user)`,
108
117
  not `isPending(user())` (that evaluates the read before `isPending` runs).
118
+ Granularity is the expression (`isPending(() => item.quantity)` with
119
+ `affects(item, "quantity")`). A hold with no feedback at all is `[SILENT_HOLD]`
120
+ under attribution — always pair a held write with one of these questions, an
121
+ optimistic value, or `affects()`.
109
122
  "Prop with local edits" = writable derivation:
110
123
  `createSignal(() => props.value)` or `createStore(() => props.value, fallback)`.
111
124
  `createOptimistic` is for an in-flight mutation, not a local editing session.
@@ -166,12 +179,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
166
179
  Making a client store "real" is additive: same setters, wrap mutations in
167
180
  `action`, swap `createOptimisticStore` and a file of server functions — do
168
181
  not rewrite `App.tsx` with loading/error branches.
182
+ No request counters or `AbortController`s for dedup/cancellation — superseded
183
+ answers drop automatically. Throw unusable responses; never `{ success: false }`
184
+ result objects checked at every read.
169
185
  Mechanically enforced by `solid2-kit check` (loading-signal naming, zero-arg-call undefined checks).
170
186
  19. **Refetch is `refresh(source)`** — rarely needed, but when it is, never fake it with a
171
187
  counter/version signal read inside the computation. Input-driven refetch is automatic
172
188
  (tracked inputs re-run the computation) and subscriptions push. Legit uses: after a
173
189
  mutation inside an `action`, and explicit reload buttons. Pair with `affects(source)`
174
190
  when the reload should present as pending — a bare `refresh()` re-asks quietly.
191
+ On a derived store, `refresh(store)` re-runs the derivation (`fn`); refreshing a
192
+ plain signal accessor is a no-op. `refresh` returns a promise for the target's next
193
+ settled state (`await refresh(user)`, or `yield refresh(x)` inside an action).
175
194
  These are three different APIs: core `refresh(source)` reruns a reactive source;
176
195
  router `revalidate(getUser.key)` invalidates the query cache; server-function
177
196
  `return reload({ revalidate: "todos" })` asks the integration to refresh cached
@@ -191,11 +210,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
191
210
  `postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity); render
192
211
  modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
193
212
  cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
194
- the sync mutation *is* the prediction (an overlay, discarded on settle); put
213
+ the sync mutation *is* the prediction (an overlay, discarded on settle); write the
214
+ body as a generator (`function*` / `async function*`) suspending on `yield`, never a
215
+ plain `async` function (`solid2-kit check` flags that); put
195
216
  `pending` on the record rather than a second copy of state. Do not snapshot
196
217
  and restore, disable optimistic rows until ack, mutex rapid clicks, or freeze
197
218
  unrelated writes because one action is in flight; failed-action replay is
198
- optional. Except reactive clients (e.g. Convex) whose subscriptions already push authoritative
219
+ optional. Fire-and-forget confirmations wait with `yield until(predicate,
220
+ { timeout })` — the predicate reads authoritative state. Except reactive clients (e.g. Convex) whose subscriptions already push authoritative
199
221
  state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
200
222
  Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`) — run `solid2-kit doctor`
201
223
  after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
@@ -205,6 +227,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
205
227
  `clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
206
228
  render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
207
229
  visible content — `clientOnly` is for components that must never run on the server.
230
+ A single browser-only *value* takes `ssrSource` on the memo/signal/derived store
231
+ (`"client"` skips the server value and computes after hydration, e.g. `localStorage`;
232
+ `"hybrid"` re-runs after adopting the serialized value, e.g. window size mixed with
233
+ server data) — do not split a component via `clientOnly` for one value. An
234
+ `onSettled` fill-in remains a valid alternative.
208
235
  Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
209
236
  (`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
210
237
  container already holds SSR HTML; `render` for an empty mount. Client mode (the
@@ -240,14 +267,20 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
240
267
  at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
241
268
  `query` are URL-addressable POST forms and a read cache — not core `action` /
242
269
  `refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
243
- (POST only; `.with(id)` binds args into the URL). `useAction` is JS-only. Cache
270
+ (POST only; `.with(id)` binds args into the URL). Validate on the server
271
+ (`throw respond({ issues }, { status: 400 })`); inline errors from `useSubmissions`,
272
+ pending via `form[aria-busy]` CSS. Keep server functions in `src/data/`, not under
273
+ `src/routes` (a `.ts` file there is an API route). `useAction` is JS-only. Cache
244
274
  reads: `query(fn, "name")` then `createMemo(() => getUser(id()))`; after a mutation
245
275
  `revalidate(getUser.key)`. Do not wrap mutations in `query` (an undeclared server
246
276
  function becomes GET). `Router.paths` is not the current location (`useLocation` /
247
- `useParams`); a route `preload` result is `props.data`. One router instance —
277
+ `useParams`); search params via `useSearchParams` (merging setter; typed with a route
278
+ `search` schema + path node). A route `preload` result is `props.data`. One router instance —
248
279
  Solid Router does not support nested `<Router>` / nested `createRouter`.
249
280
  In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
250
- `window.location` / `history.pushState`. Trusted identity is
281
+ `window.location` / `history.pushState`. Link state is automatic
282
+ (`aria-current` / `data-active` / `data-pending` + CSS; `useLinkState` /
283
+ `useIsRouting` in JSX) — never hand-rolled `location.pathname` comparisons. Trusted identity is
251
284
  `getRequestEvent()`, never a caller-supplied user id. Unscripted forms use
252
285
  the function `.url` (or a router `action`), not a hand-built `/_server/`
253
286
  path. Client history adapters do not select the SSR URL — pass
@@ -258,8 +291,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
258
291
  (`<meta name="description">`) that Solid Meta should manage.
259
292
  Return `respond(value, { status, headers })` from a `"use server"` function, not
260
293
  `Response.json` — a raw `Response` is HTTP-handler control flow; scripted
261
- callers should receive the value. Cache GET reads with `Cache-Control` on that
262
- response, not a hand-rolled Solid cache. HTTP does not enforce TypeScript —
294
+ callers should receive the value. Reads are function calls too (`await getUser(id)`
295
+ on a `GET()`-declared read; the method mirrors the operation). Return or throw a
296
+ redirect (`if (!session?.userId) throw redirect("/sign-in")`); `redirect()` /
297
+ `respond()` take `revalidate` like `reload()`. Cache GET reads with
298
+ `Cache-Control` on that response (the default is `no-store`), not a hand-rolled
299
+ Solid cache. HTTP does not enforce TypeScript —
263
300
  validate inside the function; do not invent tRPC / RPC type-gen.
264
301
  Locals only the `"use server"` body reads (db, secrets) stay off the client.
265
302
  Do not add an API-route file just to wrap a database call — `"use server"`
@@ -334,7 +371,7 @@ directory (default `src/`).
334
371
  | `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
335
372
  | `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
336
373
  | `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
337
- | JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)`, plain `<a href={Router.paths...}>`, `hashHistory()` |
374
+ | JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)` (`fileRoutes` from `@solidjs/router/fs`, `pageRoutes` from `virtual:file-routes`), plain `<a href={Router.paths...}>`, `hashHistory()`; link state via automatic `aria-current` / `data-active` / `data-pending` + CSS (`useLinkState` in JSX) |
338
375
  | `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
339
376
  | `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
340
377
  | `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
@@ -73,6 +73,7 @@ Most React `useEffect` code should NOT become `createEffect`:
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)`) |
75
75
  | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
76
+ | Browser-only *value* (`localStorage`, viewport size) | `createMemo(..., { ssrSource: "client" })` — per-value policy at the data source, no component split (`"hybrid"` when server data mixes with client signals) |
76
77
  | Server vs browser branch | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` |
77
78
  | SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
78
79
 
@@ -223,7 +224,9 @@ createEffect(
223
224
  Reads inside `apply` do not track. Extract every needed reactive value in `compute`
224
225
  (e.g. `() => ({ name: user.name, role: user.role })`). Passing a store proxy into `apply`
225
226
  and reading `user.name` there *runs once* and never retriggers. Compute-phase errors can be
226
- intercepted with the bundle form `createEffect(compute, { effect, error })`.
227
+ intercepted with the bundle form `createEffect(compute, { effect, error })`. Skip the
228
+ initial run with `{ defer: true }` — the `on(deps, fn, { defer: true })` replacement;
229
+ the effect first runs on the next change.
227
230
 
228
231
  A two-phase effect whose apply only calls a local signal setter is "state + effect" in
229
232
  disguise — formally legal Solid 2, still rule 4 (`solid2-kit check` flags it). The reset
@@ -247,6 +250,10 @@ const [signInError, setSignInError] = createSignal<string | null>(() => {
247
250
  });
248
251
  ```
249
252
 
253
+ The writable form spells exactly this: derive from the source, allow a local
254
+ override, reset when the source changes. (React needs `useState(props.x)` plus a
255
+ `previousX` comparison reset during render for the same behavior.)
256
+
250
257
  ### Async data — fetch high, block low
251
258
 
252
259
  Creating and reading an async value are the same as sync. **Consuming** (the
@@ -330,11 +337,18 @@ return <StoryDetail story={story} />;
330
337
  (`isPending(() => props.story)`), on a derived memo (`isPending(byline)`), or
331
338
  *above* the fetch but below the write (`isPending(selectedId)`), even when the
332
339
  fetch lives in a child. You do not need to pass the memo accessor just to ask.
340
+ Granularity is the expression: `isPending(() => item.quantity)` (paired with
341
+ `affects(item, "quantity")`) or `isPending(() => todos.length)` dims only what
342
+ that read depends on. A hold with no feedback at all is `[SILENT_HOLD]` under
343
+ attribution — always pair a held write with one of these questions, an
344
+ optimistic value, or `affects()`.
333
345
  - Default navigation **holds**. `selectedId()` and the old story stay put until
334
346
  the new answer lands, so the highlight never points at the wrong content.
335
347
  `latest(selectedId)` is the opt-in when the design wants the highlight to move
336
348
  on click. Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
337
- `transition-delay` so fast swaps do not flash.
349
+ `transition-delay` so fast swaps do not flash. Inputs that feed a request follow
350
+ the same rule: bind `value={latest(query)}` so a controlled search box reflects
351
+ keystrokes while its write is held (an uncontrolled input needs nothing).
338
352
  - Read every reactive input **before the first `await`** in an async computation. A read
339
353
  after `await` does not subscribe; production can sit pending with no retry.
340
354
  - `<Errored>` function fallbacks receive an error **accessor** and a `reset` callback:
@@ -354,7 +368,12 @@ reflexes that bypass the async model and `solid2-kit check` flags them. If a val
354
368
  make it an async computation and read it under boundaries. Refetching is `refresh(source)`
355
369
  (typically as the last step of a mutation `action`, or a reload button) — never a
356
370
  counter/version signal read inside the computation to force re-runs; input-driven refetch is
357
- already automatic.
371
+ already automatic. No request counters or `AbortController`s for dedup/cancellation
372
+ either — superseded answers are dropped automatically and settled content holds;
373
+ debounce at the event handler (`onInput={debounce(...)}`), never as an effect copying
374
+ one signal into another. And throw unusable responses instead of returning
375
+ `{ success: false }` objects checked at every read — failures travel to `<Errored>`
376
+ like values.
358
377
 
359
378
  **Normalize thrown values once.** Thrown values are `unknown` in JavaScript, so define one
360
379
  shared `ErrorFallback` component per app — extract the message via `instanceof Error`, wire
@@ -409,6 +428,35 @@ state — it is an overlay the graph discards when the action settles (pass or f
409
428
  There is no rollback to write. Put in-flight affordances on the record
410
429
  (`pending?: boolean`); they vanish with the overlay.
411
430
 
431
+ Minimal shape — the durable value derives from `fn`, optimistic writes overlay it,
432
+ and `refresh` re-runs the derivation so the store reconciles against the server
433
+ (no separate `getMessages()` call):
434
+
435
+ ```tsx
436
+ import { action, createOptimisticStore, refresh } from 'solid-js';
437
+
438
+ function createMessages() { // call from a component — not module scope (SSR shares modules)
439
+ const [messages, setMessages] = createOptimisticStore(() => getMessages(), []);
440
+
441
+ const addMessage = action(function* (message: Message) {
442
+ setMessages((m) => { m.push(message); });
443
+ yield saveMessage(message);
444
+ refresh(messages);
445
+ });
446
+ return [messages, { addMessage }] as const;
447
+ }
448
+ ```
449
+
450
+ Mutate the draft (`m.push(message)`), never spread-rebuild (`[...m, message]`) —
451
+ stores track writes finely, so only the changed part updates. When the action
452
+ settles, the overlay is discarded and the refreshed server data is authoritative;
453
+ if it agrees with the prediction, reconciliation finds no differences and nothing
454
+ re-renders. Derived reconciliation keys by `"id"` by default — pass `{ key: ... }`
455
+ only when the identity field differs.
456
+
457
+ Layered form, when per-row UI must survive overlay discard (a recoverable error
458
+ with Retry on that row only):
459
+
412
460
  ```tsx
413
461
  import { action, createOptimisticStore, refresh } from 'solid-js';
414
462
 
@@ -480,6 +528,28 @@ snapshot/restore.
480
528
  **`yield`, not a bare `await`, is the transaction boundary.** `await api.save()` then
481
529
  `setTodos(...)` *runs*, but the write commits immediately — it is not held as an
482
530
  overlay. Either `yield saveTodo(todo)` or, when you need a typed result, `const saved = await api.save(); yield; setTodos(...)`.
531
+ Write the body as a generator — `function*` or `async function*`, never a plain
532
+ `async (...) =>` — because only `yield` re-enters the transaction: the runtime has no
533
+ hook into a plain `await` continuation, so even inside `async function*` an `await`
534
+ leaves the transaction until the next `yield` (`solid2-kit check` flags the
535
+ non-generator form).
536
+
537
+ When confirmation arrives outside the response — a fire-and-forget transport echoed
538
+ back on a live source — hold the action open with `yield until(predicate, { timeout })`:
539
+
540
+ ```tsx
541
+ const send = action(function* (text: string) {
542
+ const clientId = crypto.randomUUID();
543
+ setMessages((m) => { m.push({ clientId, text, pending: true }); });
544
+ yield socket.send({ clientId, text });
545
+ yield until(() => messages.some((m) => m.clientId === clientId), { timeout: 10_000 });
546
+ });
547
+ ```
548
+
549
+ The predicate reads authoritative state, so the optimistic row cannot satisfy it;
550
+ only the echo can. Timeout rejects with `TimeoutError` (strongly recommended on
551
+ droppable transports; abort via `signal`). Outside actions, `await until(() => user())`
552
+ waits for a value before continuing.
483
553
 
484
554
  Navigation-shaped updates do **not** need core `action`. A plain setter is enough:
485
555
  reads pull the async, and downstream async computeds hold previous values until the
@@ -632,6 +702,46 @@ do not `return () => cleanup`. Put owned setup in a **directive factory** (like
632
702
  `ResizeObserver`, third-party widgets, and native listener options (`capture` /
633
703
  `passive`) — Solid event props do not take those options.
634
704
 
705
+ ### Per-value SSR policy: `ssrSource`
706
+
707
+ `clientOnly` splits a *component*; `ssrSource` keeps one *value* off the server,
708
+ decided at the data source. `createMemo`, function-form `createSignal`, and derived
709
+ stores (`createStore(fn, seed)`, `createProjection`, `createOptimisticStore(fn, seed)`)
710
+ all accept `ssrSource: "server" | "hybrid" | "client"`:
711
+
712
+ ```tsx
713
+ import { Loading, createMemo } from 'solid-js';
714
+
715
+ function DraftPreview(props: { documentId: string }) {
716
+ const draft = createMemo(
717
+ () => localStorage.getItem(`draft:${props.documentId}`),
718
+ { ssrSource: 'client' },
719
+ );
720
+
721
+ return (
722
+ <section>
723
+ <h2>Saved draft</h2>
724
+ <Loading fallback={<p>Checking for a saved draft...</p>}>
725
+ <p>{draft() ?? 'No saved draft.'}</p>
726
+ </Loading>
727
+ </section>
728
+ );
729
+ }
730
+ ```
731
+
732
+ | Policy | Meaning | Use when |
733
+ |---|---|---|
734
+ | `"server"` (default) | Client adopts the serialized server value; compute does not re-run | Compute is deterministic from server-available inputs |
735
+ | `"hybrid"` | Client adopts the serialized value, then re-runs the compute to take over | Server data mixes with client signals (window size, user locale) |
736
+ | `"client"` | Server value skipped; compute runs after hydration as if first-mounted | Serialization is meaningless (`localStorage`, viewport) |
737
+
738
+ With `"client"`, the bare form suspends on the server and the nearest `<Loading>`
739
+ fallback renders into the HTML; a declared first value (`loadingValue` on
740
+ signal-family sources, `seedLoadingValue: true` on store-family sources) renders
741
+ provisional data with no boundary involvement instead. Filling the value in from
742
+ an `onSettled` callback remains a valid alternative; `NoHydration` / `Hydration`
743
+ split hydration *ownership* instead and do not express this policy.
744
+
635
745
  ## Scheduling and tests
636
746
 
637
747
  Signal/store writes outside a synchronous flush scope are **staged**; the reactive queue
@@ -648,6 +758,8 @@ Wait for an async expression to settle in tests: `await resolve(() => value())`.
648
758
  on a pending resource. `onSettled` is also a **single** fire (untracked): it is not
649
759
  `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,
650
760
  writes from owned scopes, and async reads outside `<Loading>`. Fix them; don't suppress.
761
+ A read at component-body top level reports `[STRICT_READ_UNTRACKED]` naming the
762
+ component — move the read into JSX, a memo, or an effect compute.
651
763
 
652
764
  Component tests use `@solidjs/testing-library`. Pass a **function** to `render` so
653
765
  the tree has an owner, and `flush()` after interactions that stage writes:
@@ -675,6 +787,60 @@ To turn those dev diagnostics into a hard gate, patch `console.warn` in the test
675
787
  file to rethrow unexpected warnings — then any top-level reactive read or unowned write
676
788
  an agent sneaks in fails the suite instead of scrolling by.
677
789
 
790
+ ### Dev diagnostics and attribution
791
+
792
+ Diagnostics come in two tiers, both dev-build only (production strips them and `DEV`
793
+ is `undefined` there). Always-on findings include misplaced reads
794
+ (`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`), misplaced writes
795
+ (`[REACTIVE_WRITE_IN_OWNED_SCOPE]`, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`), and
796
+ `[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it.
797
+ Each entry carries a stable `[CODE]`, what the runtime observed, and an owner chain
798
+ (`in <App> › <Cart> › <LineItem>`); never silence a code before understanding it.
799
+
800
+ Cost and responsiveness findings need the opt-in attribution engine, which records
801
+ every scope that re-ran, what changed to cause it, and how long it took:
802
+
803
+ ```ts
804
+ import { DEV } from 'solid-js';
805
+
806
+ DEV?.attribution.enable();
807
+ // reproduce the interaction, then ask why a scope ran:
808
+ for (const event of DEV!.attribution.why(total)) {
809
+ console.log(DEV!.attribution.format(event));
810
+ }
811
+ ```
812
+
813
+ While attribution is enabled, Solid also warns on its own about over-subscription,
814
+ chained async waterfalls (`[ASYNC_WATERFALL]`), memos whose fresh-but-equal output
815
+ never settles (`[UNSTABLE_MEMO_OUTPUT]`), effects that relay state
816
+ (`[EFFECT_RELAY_TEAR]`) or feed their own inputs (`[EFFECT_WRITES_OWN_SOURCE]`),
817
+ immutable updates inside stores (`[IMMUTABLE_UPDATE_IN_STORE]` — mutate the draft),
818
+ row-rebuilding lists (`[UNSTABLE_LIST_IDENTITY]` — key by id or reconcile into a
819
+ store), and silent holds (`[SILENT_HOLD]` — a write held on async work with no
820
+ `isPending()` / `latest()` reader, optimistic value, `affects()` mark, or effect that
821
+ ran while held; reported from 300ms as info, from 500ms as a warning). `DEV.attribution.costs()`,
822
+ `holds()`, and `feedback()` rank the session worst-first. Name the scopes you intend
823
+ to interrogate (`createMemo(..., { name: "total" })`); `DEV.attribution.subscriptions(scope)`
824
+ lists a scope's current dependencies to compare against what it uses.
825
+
826
+ For regression tests, `@solidjs/diagnostics` turns the same channels into assertions:
827
+
828
+ ```ts
829
+ import { captureArtifact } from '@solidjs/diagnostics';
830
+
831
+ const { artifact } = await captureArtifact(() => {
832
+ fireEvent.click(getByRole('button', { name: 'Add' }));
833
+ flush();
834
+ }, { scenario: 'add-item' });
835
+
836
+ expect(artifact).toHaveNoDiagnostics();
837
+ expect(artifact).toHaveNoSilentHolds();
838
+ ```
839
+
840
+ `toHaveNoDiagnostics` fails on any coded warning (`info` findings excluded);
841
+ `toStayWithinRerunBudget` / `toHaveNoWaste` catch recompute regressions. See the
842
+ debugging-reactivity guide in [references/official-docs.md](references/official-docs.md).
843
+
678
844
  ## Client mode: no server HTML, no hydration reflexes
679
845
 
680
846
  The `@solidjs/vite-plugin` default (client mode, the `bare` tier) serves an empty-body
@@ -687,7 +853,7 @@ deferred "safe" initial value is worse than unnecessary: if anything persists th
687
853
  (an effect writing `localStorage`), the placeholder **overwrites the user's stored
688
854
  preference** before the real value is adopted. Mismatch only becomes a concept in
689
855
  start/SSR mode, where `hydrate()` mounts onto server HTML — and the Solid answer there is
690
- `isServer` / `clientOnly`, still not React-style deferred adoption.
856
+ `isServer` / `clientOnly` / per-value `ssrSource`, still not React-style deferred adoption.
691
857
 
692
858
  ## App stack (only if the project has these packages)
693
859
 
@@ -736,7 +902,7 @@ export default function App() {
736
902
  <Router>
737
903
  {(props) => (
738
904
  <>
739
- <a href={Router.paths()}>Home</a>
905
+ <a href={Router.paths}>Home</a>
740
906
  <main>{props.children}</main>
741
907
  </>
742
908
  )}
@@ -759,6 +925,27 @@ Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks:
759
925
  disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
760
926
  `"native"` / `"preload"`. One router per app.
761
927
 
928
+ Link state needs no component code: the router sets `aria-current="page"` (exact
929
+ match), `data-active` (exact or descendant — the `/` link is active only on exact
930
+ match), and `data-pending` (in-flight navigation target) on the anchors it handles;
931
+ style them in CSS. For non-anchors, or state needed in JSX, `useLinkState(() => href,
932
+ { end })` returns the same three as accessors (`active` / `current` / `pending`);
933
+ `useIsRouting()` is the page-level pending flag for progress bars. Hand-rolled
934
+ `location.pathname === ...` comparisons are the wrong tool.
935
+
936
+ Build URLs from `paths`: static segments are properties (`Router.paths.about`, never
937
+ `Router.paths.about()`), parameterized segments are calls (`paths.products(id)`),
938
+ followed by an optional search object and hash (`paths.search({ q, page }, "results")`).
939
+ Nodes stringify in `href` / `navigate()` / `redirect()`, so call with no arguments
940
+ only when an API insists on a plain string.
941
+
942
+ Search params are the home for shareable state (filter, sort, page): `const [search,
943
+ setSearch] = useSearchParams()` — the setter merges and navigates without scrolling;
944
+ `""` / `undefined` / `null` removes a key. For typed values, put a synchronous
945
+ Standard Schema on the route's `search` field and pass the path node:
946
+ `useSearchParams(Router.paths.search)` (async schemas throw; a failing schema leaves
947
+ the raw strings rather than throwing).
948
+
762
949
  **Two different `action`s.** Core `action` from `solid-js` is a generator transaction
763
950
  (optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
764
951
  a URL-addressable POST mutation (`useSubmissions`, `<form action={save} method="post">`).
@@ -783,6 +970,59 @@ Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
783
970
  action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
784
971
  drops the no-JS form fallback. `useAction` is JS-only for the same reason.
785
972
 
973
+ Validated forms come in three passes sharing one HTML shape. Pass 1 (above) posts
974
+ uncontrolled inputs with browser `required` / `pattern` — it works with no JavaScript.
975
+ Pass 2 validates on the server, where the user cannot bypass it:
976
+
977
+ ```ts
978
+ import { redirect, respond } from '@solidjs/web';
979
+ import * as v from 'valibot';
980
+
981
+ const Address = v.object({
982
+ postalCode: v.pipe(v.string(), v.regex(/^[0-9]{5}$/, 'Enter a five-digit postal code')),
983
+ });
984
+ export type AddressIssues = Partial<Record<'postalCode', string>>;
985
+
986
+ export async function saveAddress(form: FormData) {
987
+ 'use server';
988
+ const parsed = v.safeParse(Address, Object.fromEntries(form));
989
+ if (!parsed.success) {
990
+ const issues: AddressIssues = {};
991
+ for (const issue of parsed.issues) {
992
+ const field = issue.path?.[0]?.key as keyof AddressIssues | undefined;
993
+ if (field && !issues[field]) issues[field] = issue.message;
994
+ }
995
+ throw respond({ issues }, { status: 400 });
996
+ }
997
+ await database.addresses.save(parsed.output);
998
+ return redirect('/checkout/shipping');
999
+ }
1000
+ ```
1001
+
1002
+ Keep server functions in `src/data/`, not under `src/routes` (with method discovery
1003
+ on, a `.ts` file there is an API route). `throw respond(...)` is intentional control
1004
+ flow — the status and value survive production sanitization, unlike a plain thrown
1005
+ `Error`. Pass 3 renders the messages inline from the recorded submission:
1006
+
1007
+ ```tsx
1008
+ import { action, useSubmissions } from '@solidjs/router';
1009
+
1010
+ const submitAddress = action(saveAddress);
1011
+ const submissions = useSubmissions(submitAddress);
1012
+ const issues = () => (submissions.at(-1)?.error as { issues?: AddressIssues } | undefined)?.issues ?? {};
1013
+
1014
+ <Show when={issues().postalCode}>{(message) => <p role="alert">{message()}</p>}</Show>
1015
+ ```
1016
+
1017
+ `issues()` is a plain derived function; `aria-invalid` on the input, `role="alert"`
1018
+ on the message. Redirect outcomes do not stay in the list; the no-JS outcome arrives
1019
+ via flash cookie as the same submission shape. Pending state is CSS on the automatic
1020
+ attribute — `form[aria-busy] button[type="submit"] { opacity: .6 }` — no component
1021
+ code. `submission.clear()` on dismiss/resubmit only, never from an effect (errors
1022
+ would flash and vanish). Live per-keystroke validation is a controlled input plus a
1023
+ memo message (`aria-live="polite"`), keeping `name` so `FormData` still works; leave
1024
+ the rest uncontrolled.
1025
+
786
1026
  **`query` is the read cache** (optional, only with the router). Wrap a read,
787
1027
  give it a stable name, and consume it through a Solid async primitive:
788
1028
 
@@ -811,7 +1051,8 @@ router mutation, `revalidate(...)` — not core `refresh()`, and not
811
1051
  refresh cached data: `return reload({ revalidate: "todos" })`). `live()` sources
812
1052
  update through the open stream; do not `revalidate` them. Replace SolidStart /
813
1053
  Router 0.x leftovers: no `createAsync`, `useSubmission` (singular), `cache()`,
814
- router `json()`, or `<FileRoutes />` (`fileRoutes(pageRoutes)` instead).
1054
+ router `json()`, or `<FileRoutes />` (`fileRoutes(pageRoutes)` from `@solidjs/router/fs`
1055
+ over `pageRoutes` from `virtual:file-routes` instead).
815
1056
 
816
1057
  ### Server functions — `"use server"`
817
1058
 
@@ -836,7 +1077,9 @@ reference runs in-process (no HTTP); in the browser it is HTTP. Anything
836
1077
  referenced only inside the `"use server"` body (db client, secrets) never
837
1078
  reaches the client — the directive is the privacy boundary. Mutations that should
838
1079
  revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
839
- guides). After a router/server mutation do **not** follow with a client `fetch`
1080
+ guides). Return or throw a redirect — guards read `if (!session?.userId) throw
1081
+ redirect("/sign-in")`. `redirect()` and `respond()` accept the same `revalidate`
1082
+ 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`
840
1083
  to "refresh the page" — `reload` / `revalidate` (and the router's single-flight
841
1084
  response) close that loop. For 404 during SSR: `httpStatus(404)` from `@solidjs/web`.
842
1085
  `throw new Error("…")` is stripped to `"Internal Server Error"` in production —
@@ -847,7 +1090,13 @@ caller unwraps. JSON-encodable arguments only unless `enableRichArguments()`
847
1090
  was called once in the client entry (`Date` / `Map` / `Set` throw without it).
848
1091
  `GET()` is only for idempotent reads (URLs leak into logs/history);
849
1092
  import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
850
- Cache GET reads with HTTP, not a hand-rolled Solid cache: the caller still
1093
+ Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
1094
+ as `await getUser(id)` — Solid owns the endpoint, serialization, and decoding, the
1095
+ database access stays on the server, and argument/return types flow to the caller.
1096
+ The HTTP method mirrors the operation (reads GET, mutations POST); when encoded
1097
+ arguments would exceed the URL limit the client falls back to a read-only POST.
1098
+ Cache GET reads with HTTP, not a hand-rolled Solid cache: responses default to
1099
+ `Cache-Control: no-store`, so supply the policy via `respond()` — the caller still
851
1100
  receives the value; headers ride the transport.
852
1101
 
853
1102
  ```ts
@@ -943,8 +1192,9 @@ how to reuse. Prefer the form on the right.
943
1192
  | `const rest = { ...props }` | `const rest = omit(props, "label")` |
944
1193
  | `const user = createMemo(async () => { await fetch(...); return id(); })` | read `id()` **before** `await` |
945
1194
  | `action(async function* () { await save(); setX(v); })` | `yield save()` or `await save(); yield; setX(v)` |
1195
+ | `action(async (item) => { ... })` with core `action` from `solid-js` | `action(function* (item) { ...; yield ...; ... })` — only a generator re-enters the transaction (`solid2-kit check` flags this). Router `action` from `@solidjs/router` is the one that takes `async (form) => ...` |
946
1196
  | `todos()` on a store | `todos.length` / `todo.title` (property reads) |
947
- | `setTodos((t) => t.filter((x) => x.ok))` | mutate the draft, or `reconcile` |
1197
+ | `draft.todos = fresh` (wholesale fresh-tree assignment) | `reconcile(fresh, "id")`, or function-form `createStore` / `createProjection` (auto-keyed by `"id"`). Removal via `setTodos((t) => t.filter(...))` is fine — survivors keep identity |
948
1198
  | `createEffect(() => user, (u) => log(u.name))` | `createEffect(() => user.name, (name) => log(name))` |
949
1199
  | `<Loading fallback={<PageSkeleton />}>{/* header + data */}</Loading>` | wrap only the data slot; chrome stays outside |
950
1200
  | `<Loading on={id} fallback={...}>` (the accessor) | `on={id()}` — a value, so identity changes can show fallback |
@@ -962,6 +1212,7 @@ how to reuse. Prefer the form on the right.
962
1212
  | `<For each={rows.slice(from, from + n)}>` | `<Repeat from={from()} count={n}>` |
963
1213
  | `{count() && <Badge />}` | `<Show when={count()}>` — `0` must not render as text |
964
1214
  | `throw new Error("expired")` from a server function (prod) | `throw markSafeError(...)` or `throw respond(body, { status })` |
1215
+ | `const r = await api.x(); if (!r.ok) ...` result-object checks at every read | throw unusable responses; contain each region with `<Errored>` |
965
1216
  | `GET(async (id) => { "use server"; await db.delete(id) })` | mutations stay on POST; `GET()` is for idempotent reads |
966
1217
  | `live()` yields as an event log to append | each yield **replaces** the current answer; yield current state first |
967
1218
  | `<article innerHTML={html()}>…children…</article>` | `innerHTML` **or** children, not both |
@@ -975,6 +1226,7 @@ how to reuse. Prefer the form on the right.
975
1226
  | `renderToString(() => <App />)` for an async or lazy-route tree | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`). String render emits `<Loading>` fallbacks |
976
1227
  | `clientOnly(() => import("./Map"))` for a rarely shown widget | same + `{ lazy: true }` so the import waits until first render (default starts at declaration) |
977
1228
  | `<NoHydration><Chart /></NoHydration>` to skip SSR of a widget | `clientOnly(() => import("./Chart"))` — `NoHydration`/`Hydration` split ownership, they do not choose visible content |
1229
+ | `clientOnly` split (or `isServer` branch) for one browser-only *value* | `createMemo(() => localStorage.getItem(k), { ssrSource: "client" })` — `"hybrid"` when server data mixes with client signals |
978
1230
  | `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
979
1231
  | `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 |
980
1232
  | `createContext` + a root provider for a true app singleton | module-level signal/store (SSR: shared across requests). Context is subtree scoping |
@@ -990,6 +1242,8 @@ how to reuse. Prefer the form on the right.
990
1242
  | `const View = tab() ? A : B; return <View />` | `const View = dynamic(() => (tab() ? A : B))` — the ternary at setup freezes the choice |
991
1243
  | `<For each={resolved()}>` after `children()` | `{resolved.toArray()}` — resolved children are not a reactive data list |
992
1244
  | `<input onChange={...}>` for keystrokes | `onInput` — native `onChange` is blur/commit |
1245
+ | debounce as an effect copying one signal into another | debounce at the handler: `onInput={debounce((e) => setQuery(e.currentTarget.value), 150)}` |
1246
+ | `value={query()}` on a search box whose writes feed a request | `value={latest(query)}` so keystrokes show while the write is held (uncontrolled needs nothing) |
993
1247
  | `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
994
1248
  | `createMemo(async fn, { loadingValue })` / `{ seedLoadingValue: true }` as the default first-flight UI | `<Loading>` for first flight; those options are escape hatches (store projections use `seedLoadingValue`) |
995
1249
  | `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
@@ -997,6 +1251,9 @@ how to reuse. Prefer the form on the right.
997
1251
  | `action={"/_server/" + id}` in app JSX | `action={createTodo.url}` or a router `action` |
998
1252
  | `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
999
1253
  | `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
1254
+ | `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on automatic `aria-current` / `data-active` / `data-pending`; `useLinkState` in JSX, `useIsRouting()` for progress bars |
1255
+ | `new URLSearchParams(location.search)` hand-rolled query parsing | `useSearchParams()` (setter merges, no scroll); typed via a route `search` schema + `useSearchParams(Router.paths.search)` |
1256
+ | `Router.paths.about()` / string-concatenated URLs | static nodes are properties (`Router.paths.about`); calls bind params, then search object and hash |
1000
1257
  | `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
1001
1258
  | `try { user() } catch (e) { /* NotReadyError */ }` | `<Loading>` — app code should not catch `NotReadyError` |
1002
1259
  | `{latest(() => results())}` as the visible list | settled read + `isPending()` for the indicator; `latest` is for previews |
@@ -1015,7 +1272,10 @@ how to reuse. Prefer the form on the right.
1015
1272
  | `Object.assign({}, props)` / expecting `merge` to skip `undefined` like 1.x `mergeProps` | `omit` / `merge` — explicit `undefined` **overrides** (Object.assign) |
1016
1273
  | `isPending(user())` / `latest(user())` | `isPending(user)` / `latest(user)` — pass the accessor (or `() => user()`). Calling it first evaluates the read before the helper runs |
1017
1274
  | `isPending(...)` as the first-load spinner | `<Loading>` owns first flight; `isPending` is the *refetch* indicator after a settled answer exists |
1275
+ | a held write with no feedback anywhere | pair it with `isPending()` / `latest()` / an optimistic value / `affects()` — otherwise dev + attribution reports `[SILENT_HOLD]` |
1018
1276
  | `refresh(getUser.key)` / `revalidate(user)` / `return refresh()` from `"use server"` | three APIs: core `refresh(source)` reruns a reactive source; router `revalidate(getUser.key)` invalidates the query cache; `return reload({ revalidate: "todos" })` asks the integration to refresh cached data |
1277
+ | `return redirect(...)` as the only redirect shape | guards `throw redirect("/sign-in")`; `redirect()` / `respond()` take `revalidate` too when the mutation also moves or returns a value |
1278
+ | `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
1019
1279
  | `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
1020
1280
  | `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
1021
1281
  | `<Dynamic component={tab() ? A : B} />` | `const View = dynamic(() => (tab() ? A : B))` — `<Dynamic>` is a JSX convenience; app code should use `dynamic()` for stable identity |
@@ -1170,14 +1430,17 @@ always-applied rules installed alongside this skill.
1170
1430
  is a writable derivation (`createSignal(() => { source(); return initial; })`), not
1171
1431
  an effect calling the setter.
1172
1432
  - [ ] Event handlers wrap setters (`onClick={() => setX(...)}`), use `currentTarget`,
1173
- and `onInput` for keystrokes. `innerHTML` is sanitized and not mixed with children.
1433
+ and `onInput` for keystrokes. Debounce at the handler, never as an effect copying
1434
+ signals. `innerHTML` is sanitized and not mixed with children.
1174
1435
  - [ ] Async memos are passed as values (`user={user()}`); JSX props are lazy.
1175
1436
  Types stay `User` — not `Promise<User>` / `Accessor<User>`. `<Loading>` wraps
1176
1437
  the *read*, not the memo, not chrome. Extracting `const u = user()` at the
1177
1438
  parent is the read that throws. Nested child fetches run in parallel
1178
1439
  (components do not suspend); a real waterfall is `fetchB(a().id)`.
1179
1440
  `isPending` is per-expression (`isPending(() => props.story)` works);
1180
- `latest(selectedId)` is opt-in, not the default highlight.
1441
+ `latest(selectedId)` is opt-in, not the default highlight. Inputs feeding a request
1442
+ bind `latest(query)`; failures throw to `<Errored>`, never `{ success: false }`
1443
+ result objects checked at every read.
1181
1444
  - [ ] Lists via `<For>` (server/refetched rows keyed by stable id), conditionals via
1182
1445
  ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props; no `value()!`.
1183
1446
  - [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
@@ -1198,8 +1461,8 @@ always-applied rules installed alongside this skill.
1198
1461
  is static `dist/client`. Do not delay `renderToStream` for visual order
1199
1462
  (`<Reveal>` does).
1200
1463
  - [ ] External collections reconcile into stores (function-form `createStore` /
1201
- `reconcile`), never wholesale draft assignment or `setTodos((t) => t.filter(...))`
1202
- as the identity-preserving update.
1464
+ `reconcile`), never wholesale draft assignment of a fresh tree
1465
+ (`setTodos((t) => t.filter(...))` for removal is fine — survivors keep identity).
1203
1466
  - [ ] Rest props via `omit`, not `{ ...props }`. Conditional `class` via objects, not string concat.
1204
1467
  - [ ] Input filters write back to the DOM on reject (inputs do not rewind).
1205
1468
  - [ ] No Solid 1.x imports or APIs (`solid-js/store`, `createResource`, `onMount`, `Suspense`, ...).
@@ -1207,12 +1470,25 @@ always-applied rules installed alongside this skill.
1207
1470
  `solid-js`; DOM `JSX` types come from `@solidjs/web`.
1208
1471
  - [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
1209
1472
  component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
1210
- - [ ] Browser-only code uses `isServer` / `clientOnly`, not `typeof window`.
1473
+ - [ ] Browser-only code uses `isServer` / `clientOnly` (components) / `ssrSource`
1474
+ (values), not `typeof window`.
1211
1475
  Component teardown is `onSettled` (return cleanup), not `onCleanup`.
1476
+ - [ ] Dev console shows no diagnostics: reads in tracking scopes (no
1477
+ `[STRICT_READ_UNTRACKED]`), held writes paired with `isPending` / `latest` /
1478
+ optimistic values / `affects()` (no `[SILENT_HOLD]`). Attribution-only costs
1479
+ (`[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`, `[EFFECT_*]`)
1480
+ checked via `DEV.attribution` when touching stores, lists, or effects. Name
1481
+ interrogated scopes (`{ name: "total" }`).
1212
1482
  - [ ] If the project has a router: `createRouter({ routes })`, not JSX `<Route>` / `<A>`.
1213
1483
  One instance, no nested `<Router>`. Navigate with `useNavigate` / `<a href>`, not
1214
1484
  `window.location`. Router `action`/`query` come from `@solidjs/router` (POST forms + cache), not
1215
- core `action`/`refresh`. Forms: `<form action={save} method="post">`.
1485
+ core `action`/`refresh`. Forms: `<form action={save} method="post">`. Core `action`
1486
+ bodies are generators (`function*` / `async function*`) suspending on `yield`,
1487
+ never plain `async` functions. Link state is automatic attributes + CSS
1488
+ (`aria-current` / `data-active` / `data-pending`; `useLinkState` / `useIsRouting`
1489
+ in JSX). Search params via `useSearchParams`. Forms validate on the server
1490
+ (`throw respond({ issues }, { status: 400 })`) with inline errors from
1491
+ `useSubmissions`.
1216
1492
  - [ ] `render(() => <App />, root)` — a function, not `render(<App />)`. `hydrate` when
1217
1493
  HTML already exists. Stream async/lazy trees (`await renderToStream(...)`) with
1218
1494
  exactly one consumer (`pipe` / `pipeTo` / `readable`). Snapshot stores with
@@ -26,6 +26,7 @@ in `llms.txt` rather than guessing from Solid 1.x or React.
26
26
  - https://v2-rebuild--solid-docs-v2.netlify.app/concepts/components-and-jsx.md
27
27
  - https://v2-rebuild--solid-docs-v2.netlify.app/concepts/async-reactivity.md
28
28
  - https://v2-rebuild--solid-docs-v2.netlify.app/concepts/boundaries.md
29
+ - https://v2-rebuild--solid-docs-v2.netlify.app/concepts/mutations.md
29
30
  - https://v2-rebuild--solid-docs-v2.netlify.app/concepts/rendering-and-ssr.md
30
31
 
31
32
  ## Building apps (start mode / fullstack)
@@ -65,6 +66,10 @@ read these before writing route JSX.
65
66
  ## Guides and migration
66
67
 
67
68
  - https://v2-rebuild--solid-docs-v2.netlify.app/guides/avoid-unnecessary-effects.md
69
+ - https://v2-rebuild--solid-docs-v2.netlify.app/guides/debugging-reactivity.md
70
+ - https://v2-rebuild--solid-docs-v2.netlify.app/guides/lists.md
71
+ - https://v2-rebuild--solid-docs-v2.netlify.app/guides/data-fetching-patterns.md
72
+ - https://v2-rebuild--solid-docs-v2.netlify.app/guides/forms.md
68
73
  - https://v2-rebuild--solid-docs-v2.netlify.app/guides/testing.md
69
74
  - https://v2-rebuild--solid-docs-v2.netlify.app/migration/from-react.md
70
75
  - https://v2-rebuild--solid-docs-v2.netlify.app/migration/from-solid-1.md
@@ -108,9 +113,14 @@ verify API names against the reference pages above.
108
113
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/affects.md
109
114
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/on-settled.md
110
115
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/refresh.md
116
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/until.md
111
117
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/specialized-reactivity/on-cleanup.md
112
118
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/store-advanced/snapshot.md
113
119
 
120
+ ## Reference: diagnostics (dev builds only)
121
+
122
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/diagnostics-dev-hooks/dev.md
123
+
114
124
  ## Reference: components and JSX (solid-js)
115
125
 
116
126
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/components-jsx/show.md
@@ -160,8 +170,10 @@ verify API names against the reference pages above.
160
170
 
161
171
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router.md
162
172
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/router-factory.md
173
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/routes-and-paths.md
163
174
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/navigation.md
164
175
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/data.md
176
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/types.md
165
177
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/history.md
166
178
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/filesystem.md
167
179
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/title.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",