solid2-agent-kit 0.8.1 → 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,9 +17,39 @@ 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
- - `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or async memos
21
- passed as values (`user={user()}`) instead of accessors;
22
- - store updates that rebuild objects/arrays instead of mutating the draft or reconciling;
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) => ...`);
23
+ - `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
24
+ `const u = user()` extracted then passed (a real parent-side read). Passing
25
+ `user={user()}` is the colorless form — do not "fix" it into accessors or
26
+ `Promise<User>` props;
27
+ - `latest(selectedId)` as the default highlight (hold is the default);
28
+ `isPending` treated as a global spinner instead of a per-expression question;
29
+ - treating `<Errored>` as a terminal ErrorBoundary, or routing per-row mutation
30
+ failures through it (those belong in the action / a projection-folded map);
31
+ - nested fetches assumed to waterfall, or `<Loading>` lifted along with a lifted fetch;
32
+ - rewriting App with loading/error branches, or snapshot/restore, when wrapping
33
+ a client store in server functions; disabling optimistic rows until ack;
34
+ - tRPC / type-gen around `"use server"`, a client `fetch` after a mutation,
35
+ refetch in `hydrate`/`onSettled`, a custom Worker/Express adapter, or
36
+ delaying `renderToStream` for visual order (`<Reveal>` owns display);
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;
23
53
  - context values passed as snapshots instead of accessors/setters/stores;
24
54
  - components with conditional/early returns on reactive values.
25
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 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).
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,28 +74,51 @@ 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
- 10. **Async data is an async computation**: `createMemo(async () => ...)` read under
79
- `<Loading>` / `<Errored>` boundaries. Read every reactive input **before the first
86
+ 10. **Async data is an async computation**: `createMemo(async () => ...)` (or
87
+ `createMemo(() => fetchUser(id()))`) read under `<Loading>` / `<Errored>`.
88
+ **Fetch high, block low** — creating/reading are like sync; consuming (await)
89
+ and blocking (the boundary) are independent. Create the memo early, wrap only
90
+ the read with `<Loading>`. Do not glue the boundary to the memo, and do not
91
+ retarget intermediate props to `Promise<User>` / `Accessor<User>` — types stay
92
+ `User`. Nested trees do not suspend: they fetch in parallel; a real waterfall
93
+ looks like `fetchB(a().id)`. JSX component props are lazy (passing isn't reading):
94
+ `<Child user={user()} />` then `{props.user.name}` under the child's
95
+ `<Loading>` is the colorless form. `const u = user()` at the parent *is* a
96
+ read and throws/snapshots there. Pass the memo itself only when the child
97
+ must `refresh()` that source — `isPending(() => props.user)` works on the
98
+ value. Read every reactive input **before the first
80
99
  `await`** — post-`await` reads do not subscribe, and in production the computation can
81
100
  sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
82
101
  `<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
83
102
  during refetch (`isPending` for the indicator). Use `on={id()}` (the *value*, not the
84
- accessor) only when that identity change should show the fallback again. Pass an async
85
- memo to a child as the **accessor** (`user={user}`), and read `props.user()` under that
86
- child's `<Loading>` — `user={user()}` throws not-ready at the parent, so the child's
87
- boundary never sees it. Do not start `fetch` (or any request) at component-body top
103
+ accessor) only when that identity change should show the fallback again. Do not start `fetch` (or any request) at component-body top
88
104
  level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
89
105
  use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
90
- `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer. `isPending`
91
- is the *refetch* indicator after a settled answer exists — do not use it as the
106
+ `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer.
107
+ Default navigation holds: keep `selectedId()` so highlight and content stay
108
+ consistent; `latest(selectedId)` only when the highlight should move first.
109
+ Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
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.
112
+ `isPending` is a per-expression question, not a global spinner: ask it of the
113
+ async source, a derived prop (`isPending(() => props.story)`), or the write
114
+ (`isPending(selectedId)`), even when the fetch lives in a child. It is
115
+ the *refetch* indicator after a settled answer exists — do not use it as the
92
116
  first-load spinner (`<Loading>` owns that). Pass the accessor: `isPending(user)`,
93
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()`.
94
122
  "Prop with local edits" = writable derivation:
95
123
  `createSignal(() => props.value)` or `createStore(() => props.value, fallback)`.
96
124
  `createOptimistic` is for an in-flight mutation, not a local editing session.
@@ -143,13 +171,26 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
143
171
  integration layers. Model async as a computation (return a promise or async iterable
144
172
  from a memo/store) and let the framework own the states: first load = `<Loading>`
145
173
  fallback; refetch indicator = `isPending()`; errors = thrown into the graph and caught
146
- by `<Errored>`; speculative value = `latest()`. Mechanically enforced by
147
- `solid2-kit check` (loading-signal naming, zero-arg-call undefined checks).
174
+ by `<Errored>`; speculative value = `latest()`. `<Errored>` is graph status, not a
175
+ terminal React ErrorBoundary: it heals when the source succeeds again (refresh,
176
+ live reconnect, input change). `reset` retries the collected *sources*, not a UI
177
+ remount. Per-row mutation failures belong in an errors map the projection folds
178
+ in (survives the optimistic overlay), not in `<Errored>` around the whole list.
179
+ Making a client store "real" is additive: same setters, wrap mutations in
180
+ `action`, swap `createOptimisticStore` and a file of server functions — do
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.
185
+ Mechanically enforced by `solid2-kit check` (loading-signal naming, zero-arg-call undefined checks).
148
186
  19. **Refetch is `refresh(source)`** — rarely needed, but when it is, never fake it with a
149
187
  counter/version signal read inside the computation. Input-driven refetch is automatic
150
188
  (tracked inputs re-run the computation) and subscriptions push. Legit uses: after a
151
189
  mutation inside an `action`, and explicit reload buttons. Pair with `affects(source)`
152
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).
153
194
  These are three different APIs: core `refresh(source)` reruns a reactive source;
154
195
  router `revalidate(getUser.key)` invalidates the query cache; server-function
155
196
  `return reload({ revalidate: "todos" })` asks the integration to refresh cached
@@ -169,7 +210,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
169
210
  `postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity); render
170
211
  modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
171
212
  cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
172
- except reactive clients (e.g. Convex) whose subscriptions already push authoritative
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
216
+ `pending` on the record rather than a second copy of state. Do not snapshot
217
+ and restore, disable optimistic rows until ack, mutex rapid clicks, or freeze
218
+ unrelated writes because one action is in flight; failed-action replay is
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
173
221
  state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
174
222
  Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`) — run `solid2-kit doctor`
175
223
  after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
@@ -179,6 +227,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
179
227
  `clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
180
228
  render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
181
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.
182
235
  Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
183
236
  (`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
184
237
  container already holds SSR HTML; `render` for an empty mount. Client mode (the
@@ -214,14 +267,20 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
214
267
  at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
215
268
  `query` are URL-addressable POST forms and a read cache — not core `action` /
216
269
  `refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
217
- (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
218
274
  reads: `query(fn, "name")` then `createMemo(() => getUser(id()))`; after a mutation
219
275
  `revalidate(getUser.key)`. Do not wrap mutations in `query` (an undeclared server
220
276
  function becomes GET). `Router.paths` is not the current location (`useLocation` /
221
- `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 —
222
279
  Solid Router does not support nested `<Router>` / nested `createRouter`.
223
280
  In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
224
- `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
225
284
  `getRequestEvent()`, never a caller-supplied user id. Unscripted forms use
226
285
  the function `.url` (or a router `action`), not a hand-built `/_server/`
227
286
  path. Client history adapters do not select the SSR URL — pass
@@ -230,11 +289,36 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
230
289
  `<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`. A static
231
290
  `<title>` in the document shell is the fallback; do not hardcode other tags
232
291
  (`<meta name="description">`) that Solid Meta should manage.
233
- Return `respond(value, { status })` from a `"use server"` function, not
292
+ Return `respond(value, { status, headers })` from a `"use server"` function, not
234
293
  `Response.json` — a raw `Response` is HTTP-handler control flow; scripted
235
- callers should receive the value. JSON-encodable server-function arguments
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 —
300
+ validate inside the function; do not invent tRPC / RPC type-gen.
301
+ Locals only the `"use server"` body reads (db, secrets) stay off the client.
302
+ Do not add an API-route file just to wrap a database call — `"use server"`
303
+ is the RPC. During SSR the call is in-process (no HTTP). After a mutation, `reload` /
304
+ `revalidate` / single-flight — never a client `fetch` or an `onSettled` /
305
+ `hydrate` refetch to "refresh the page". In-flight Promises serialize as
306
+ Promises; `live()` embeds the first value in HTML and continues the stream on
307
+ the client. Do not delay `renderToStream` for visual order (`<Reveal>` owns
308
+ display). Do not write subscribe/unsubscribe in the component around `live()`.
309
+ When the app has a server bundle (`ssr` or server functions), production is
310
+ `handleRequest(request)` from the built `dist/server/server.js` (or its
311
+ Fetchable `fetch`); platform Vite plugins adopt that handler (Node: the
312
+ template `server.js`). Client-only start mode is static `dist/client` —
313
+ there is no handler to wrap. Request middleware is
314
+ `start: { middleware: "./src/middleware.ts" }`, not Express `app.use`. Do not return
315
+ components from `"use server"` unless the project already enabled the
316
+ experimental `serverFunctions.components` flag. JSON-encodable server-function arguments
236
317
  only, unless `enableRichArguments()` was called once in the client entry
237
- (`Date` / `Map` / `Set` throw without it).
318
+ (`Date` / `Map` / `Set` throw without it). `live()` connection state is
319
+ `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), never a
320
+ field in the yielded value. Sibling `<Loading>` reveal order is `<Reveal>`
321
+ (`collapsed` suppresses tail skeletons under sequential order).
238
322
 
239
323
  ## Lint heritage: eslint-plugin-solid (Solid 1.x) intents carried into this file
240
324
 
@@ -287,7 +371,7 @@ directory (default `src/`).
287
371
  | `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
288
372
  | `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
289
373
  | `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
290
- | 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) |
291
375
  | `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
292
376
  | `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
293
377
  | `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |