solid2-agent-kit 0.23.1 → 0.25.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/bin/baseline.mjs CHANGED
@@ -19,14 +19,14 @@
19
19
  // pulls them in — so doctor also checks an undeclared install of any package
20
20
  // here, which is where a lockfile holds an older compiler.
21
21
  export const BASELINE = {
22
- 'solid-js': { line: 2, baseline: '2.0.0-rc.17' },
23
- '@solidjs/web': { line: 2, baseline: '2.0.0-rc.17' },
24
- '@solidjs/signals': { line: 2, baseline: '2.0.0-rc.17' },
25
- '@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.17' },
26
- '@solidjs/compiler': { line: 2, baseline: '2.0.0-rc.17' },
27
- '@solidjs/babel-plugin': { line: 2, baseline: '2.0.0-rc.17' },
28
- '@solidjs/router': { line: 2, baseline: '2.0.0-next.42' },
29
- '@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.51' },
22
+ 'solid-js': { line: 2, baseline: '2.0.0-rc.21' },
23
+ '@solidjs/web': { line: 2, baseline: '2.0.0-rc.21' },
24
+ '@solidjs/signals': { line: 2, baseline: '2.0.0-rc.21' },
25
+ '@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.21' },
26
+ '@solidjs/compiler': { line: 2, baseline: '2.0.0-rc.21' },
27
+ '@solidjs/babel-plugin': { line: 2, baseline: '2.0.0-rc.21' },
28
+ '@solidjs/router': { line: 2, baseline: '2.0.0-next.44' },
29
+ '@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.57' },
30
30
  '@solidjs/testing-library': { line: 1, baseline: '1.0.0-rc.2' },
31
31
  };
32
32
 
@@ -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 `formAction`/`query` from `@solidjs/router` are POST forms and a read cache (router `action` is a deprecated alias of `formAction`, and `useAction` is deprecated: call the server function directly — a mounted router applies its revalidation and redirect on any call <!-- upstream:router-action-deprecated -->). React `useDeferredValue` is `createDeferred(fn)`: an async memo that lags but never leads <!-- upstream:create-deferred-export -->. 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` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the `onSettled` callback in dev. Creating a memo/effect/root inside an effect's `apply` callback throws in dev too (`[PRIMITIVE_IN_EFFECT_CALLBACK]`) — create it in the body or compute phase. <!-- upstream:primitive-in-effect-callback --> Effect `apply` functions use block bodies (`(t) => { document.title = t; }`): any return value other than a cleanup function halts reactivity. Stores are written only through the setter's draft — writes to the store itself are ignored (dev warns `[STORE_WRITE_OUTSIDE_SETTER]` <!-- upstream:store-write-outside-setter -->), and `Map`/`Set`/`Date` inside a store are not tracked. Shared state, app-wide included, is created in a provider at the root of `App` (`createContext<T>()` + `useX()`), never a module-level signal/store (one instance serves every SSR request). Errors thrown out of an `action` never reach `<Errored>` — catch them in the action or `.catch` the call. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — markup types are `JSX.Element` from `@solidjs/web` or `Element` from `solid-js` (alias it `SolidElement` next to DOM code) <!-- upstream:jsx-namespace-absent -->. 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: "client"` on the memo/signal/derived store, not a `clientOnly` component split. Router link state is `aria-current`/`data-active` + CSS (`data-pending` is opt-in via `createRouter({ links: pendingLinks })`; `useLinkState`/`useIsRouting` in JSX) <!-- upstream:pending-links-opt-in -->; link preloading is opt-in too (`preloadLinks: intentPreload()` for hover/focus/touch; none by default; opt one link out with the string `preload="false"`) <!-- upstream:preload-links-opt-in -->; search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Confirmations on another channel — fire-and-forget sends, and mutations whose result is read through `live()` — wait with `yield until(predicate, {timeout})`, not `refresh()`. Declare live reads `live(fn)` — it implies `GET` (`live(GET(fn))` is the same) <!-- upstream:live-implies-get -->. Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write) <!-- upstream:async-store-setter-throws -->, a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`, and `[UNCOMMITTED_INPUT_OVERWRITTEN]` means a `value`/`checked` binding re-wrote the bound value over text the user typed but never committed (`value={props.name} onChange={commit}` with no draft signal) — bind a draft signal written from `onInput`, or `defaultValue` <!-- upstream:uncommitted-input-overwritten -->. Error monitoring is `configureClientErrors` / `configureServerErrors` (or per root `render(fn, root, undefined, { onError })` — options are the 4th argument), not a side effect inside `<Errored fallback>`; the server hook must return a reference or nothing, never the error itself. `<Dynamic>` is deprecated — use `dynamic()`. <!-- upstream:dynamic-deprecated --> Observability is on by default in development: whenever a change touches stores, lists, async, actions, or effects, keep the dev console free of `[CODE]` findings and run an attribution session (`/__solid/diagnostics` `begin` → interact → `whyDidRun` / `costs` → `end` with `@solidjs/diagnostics` installed, otherwise `isDev`-guarded `attribution.enable({ log: false })` + `why()` / `costs()`) before finishing. `vite dev` is always the dev build; `solid({ observe: true })` is a production opt-in.
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 `formAction`/`query` from `@solidjs/router` are POST forms and a read cache (router `action` is a deprecated alias of `formAction`, and `useAction` is deprecated: call the server function directly — a mounted router applies its revalidation and redirect on any call <!-- upstream:router-action-deprecated -->). React `useDeferredValue` is `createDeferred(fn)`: an async memo that lags but never leads <!-- upstream:create-deferred-export -->. 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` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the `onSettled` callback in dev. Creating a memo/effect/root inside an effect's `apply` callback throws in dev too (`[PRIMITIVE_IN_EFFECT_CALLBACK]`) — create it in the body or compute phase. <!-- upstream:primitive-in-effect-callback --> Effect `apply` functions use block bodies (`(t) => { document.title = t; }`): any return value other than a cleanup function halts reactivity. Stores are written only through the setter's draft — writes to the store itself are ignored (dev warns `[STORE_WRITE_OUTSIDE_SETTER]` <!-- upstream:store-write-outside-setter -->), and `Map`/`Set`/`Date` inside a store are not tracked. Shared state, app-wide included, is created in a provider at the root of `App` (`createContext<T>()` + `useX()`), never a module-level signal/store (one instance serves every SSR request). Errors thrown out of an `action` never reach `<Errored>` — catch them in the action or `.catch` the call. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — markup types are `JSX.Element` from `@solidjs/web` or `Element` from `solid-js` (alias it `SolidElement` next to DOM code) <!-- upstream:jsx-namespace-absent -->. 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: "client"` on the memo/signal/derived store, not a `clientOnly` component split. Router link state is `aria-current`/`data-active` + CSS (`data-pending` is opt-in via `createRouter({ links: pendingLinks })`; `useLinkState`/`useIsRouting` in JSX) <!-- upstream:pending-links-opt-in -->; link preloading is opt-in too (`preloadLinks: intentPreload()` for hover/focus/touch; none by default; opt one link out with the string `preload="false"`) <!-- upstream:preload-links-opt-in -->; search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Confirmations on another channel — fire-and-forget sends, and mutations whose result is read through `live()` — wait with `yield until(predicate, {timeout})`, not `refresh()`. Declare live reads `live(fn)` — it implies `GET` (`live(GET(fn))` is the same) <!-- upstream:live-implies-get -->. Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write) <!-- upstream:async-store-setter-throws -->, a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]` (a write that must not join another update's hold goes in `onFlush(...)` <!-- upstream:on-flush -->), and `[UNCOMMITTED_INPUT_OVERWRITTEN]` means a `value`/`checked` binding re-wrote the bound value over text the user typed but never committed (`value={props.name} onChange={commit}` with no draft signal) — bind a draft signal written from `onInput`, or `defaultValue` <!-- upstream:uncommitted-input-overwritten -->. Error monitoring is `configureClientErrors` / `configureServerErrors` (or per root `render(fn, root, undefined, { onError })` — options are the 4th argument), not a side effect inside `<Errored fallback>`; the server hook must return a reference or nothing, never the error itself. `<Dynamic>` is deprecated — use `dynamic()`. <!-- upstream:dynamic-deprecated --> Observability is on by default in development: whenever a change touches stores, lists, async, actions, or effects, keep the dev console free of `[CODE]` findings and run an attribution session (`/__solid/diagnostics` `begin` → interact → `whyDidRun` / `costs` → `end` with `@solidjs/diagnostics` installed, otherwise `isDev`-guarded `attribution.enable({ log: false })` + `why()` / `costs()`) before finishing. `vite dev` is always the dev build; `solid({ observe: true })` is a production opt-in.
@@ -209,7 +209,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
209
209
  the callback. The `onSettled` callback is a restricted scope: `onCleanup`
210
210
  (`[CLEANUP_IN_FORBIDDEN_SCOPE]`), creating a memo or effect
211
211
  (`[PRIMITIVE_IN_FORBIDDEN_SCOPE]`), and `flush()` all throw there in dev and halt
212
- the reactive system. A 1.x `onMount(() => { …; onCleanup(…) })` becomes
212
+ the reactive system; a pending async read there warns `[PENDING_ASYNC_FORBIDDEN_SCOPE]`
213
+ <!-- upstream:pending-async-forbidden-scope --> (nothing re-runs the callback when the answer lands). A 1.x `onMount(() => { …; onCleanup(…) })` becomes
213
214
  `onSettled(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); })`.
214
215
  13. **Writes are staged** and commit on the next microtask. Event handlers need nothing
215
216
  special; tests and imperative integration code must call `flush()` before observing
@@ -550,7 +551,7 @@ directory (default `src/`).
550
551
  | `import ... from "solid-js/store"` or `"solid-js/web"` | stores/`merge`/`omit` from `"solid-js"`; `render`/`hydrate`/`Portal`/`Dynamic` from `"@solidjs/web"` |
551
552
  | `import type { JSX } from "solid-js"` | `solid-js` exports no `JSX` namespace: markup types are `JSX.Element` from `"@solidjs/web"` (with `JSX.IntrinsicElements`, `JSX.CSSProperties`) or `Element` from `"solid-js"` — import the latter as `type Element as SolidElement` where the DOM `Element` is also used, or it shadows it <!-- upstream:jsx-namespace-absent --> |
552
553
  | `createResource` | async `createMemo` + `<Loading>`/`<Errored>`; `refresh()`, `latest()`, `isPending()` |
553
- | `createEffect(fn)` (one arg), `on(...)`, `createTrackedEffect` (deprecated in 2.0) <!-- upstream:tracked-effect-deprecated --> | `createEffect(compute, apply)`; deps belong in `compute`; one-time DOM work is `onSettled` |
554
+ | `createEffect(fn)` (one arg — dev throws `[MISSING_EFFECT_FN]` <!-- upstream:missing-effect-fn -->), `on(...)`, `createTrackedEffect` (deprecated in 2.0) <!-- upstream:tracked-effect-deprecated --> | `createEffect(compute, apply)`; deps belong in `compute`; one-time DOM work is `onSettled` |
554
555
  | `onMount` | `onSettled(() => { ...; return cleanup })` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the callback |
555
556
  | `batch(...)` | delete it — writes auto-batch; `flush()` only to observe synchronously |
556
557
  | `<Suspense>`, `<ErrorBoundary>`, `<SuspenseList>` | `<Loading>`, `<Errored>` (fallback gets an error *accessor*), `<Reveal>` |
@@ -564,7 +565,7 @@ directory (default `src/`).
564
565
  | `createDeferred(source, { timeoutMs })` (1.x: a signal that lags by idle time) | `createDeferred(fn)` (2.0: an async memo that may lag its inputs but never leads them; `isPending(deferred)` while it catches up) <!-- upstream:create-deferred-export --> |
565
566
  | `startTransition`, `useTransition` | automatic held updates + `isPending` |
566
567
  | `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
567
- | `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
568
+ | `from(...)` / `observable(...)` (the solid-js helpers) | inbound: `from(source)` — the same name in 2.0, but it returns an AsyncIterable for a computation (`createMemo(() => from(source))`), not a signal <!-- upstream:from-source-adapts -->; outbound: split effect |
568
569
  | `createDynamic(...)`, `<Dynamic component={...}>` (deprecated in 2.0) <!-- upstream:dynamic-deprecated --> | `dynamic(source)` from `@solidjs/web` — `dynamicComponent(source)` from `solid-js` when the source only answers with a component <!-- upstream:dynamic-component-export --> |
569
570
  | `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
570
571
  | `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
@@ -582,6 +583,11 @@ When unsure about any API, verify against the official Solid 2.0 docs — fetcha
582
583
  listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
583
584
  Solid 1.x or React memory.
584
585
 
586
+ `createOwner` is not public API either: since 2.0.0-rc.21 it is typed only through
587
+ `solid-js/internal` (a runtime export kept for the renderers) <!-- upstream:create-owner-internal --> — host a
588
+ scope with `createRoot()`, pass its `dispose` around as a plain function, and re-enter
589
+ with `runWithOwner(owner, …)`.
590
+
585
591
  `createRenderEffect` exists in Solid 2 but is not a default. `storePath`
586
592
  (the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
587
593
  2.0.0-rc.9 — convert path setters to draft setters instead of importing it from
@@ -44,7 +44,7 @@ Practical consequences:
44
44
  | App-wide state (theme, session, cart, locale) | the same provider, placed at the root of `App` — not a module-level signal/store (under SSR one module instance serves every request) |
45
45
  | Constants (a category list, a formatter, the `createContext` call) | module scope |
46
46
  | Tentative value during a mutation | `createOptimistic` / `createOptimisticStore` + `action` |
47
- | Values pushed by an external subscription (websocket, reactive client) | async iterable returned from a memo (or function-form `createStore` for keyed reconciliation) — never `{ data, error }` signal pairs |
47
+ | Values pushed by an external subscription (websocket, reactive client) | async iterable returned from a memo — `from(source)` adapts a callback subscription, an Observable, or an AsyncIterable <!-- upstream:from-source-adapts --> (or function-form `createStore` for keyed reconciliation) — never `{ data, error }` signal pairs |
48
48
 
49
49
  ### Should this be an effect?
50
50
 
@@ -307,7 +307,10 @@ returns the value it set. Dev throws `effect callback returned an invalid cleanu
307
307
  production throws a `TypeError` on the next run, and both halt the whole
308
308
  reactive system (`[REACTIVITY_HALTED]`). TypeScript reports it as TS2345; a JS project
309
309
  gets no warning. The cleanup of an effect is the `apply` return value — `onCleanup`
310
- inside `apply` has no owner and never runs (`[NO_OWNER_CLEANUP]`).
310
+ inside `apply` has no owner and never runs (`[NO_OWNER_CLEANUP]`). The two-phase
311
+ signature itself is enforced: `createEffect(compute)` with no apply function throws
312
+ `[MISSING_EFFECT_FN]` in dev <!-- upstream:missing-effect-fn --> (prod dies on the same call with an uncoded
313
+ `TypeError`).
311
314
 
312
315
  **Effects never compare.** `apply` runs on every re-run of `compute`, whether or not the
313
316
  computed value changed; there is no `equals` on an effect. When equal results should skip
@@ -330,6 +333,13 @@ intercepted with the bundle form `createEffect(compute, { effect, error })`. Ski
330
333
  initial run with `{ defer: true }` — the `on(deps, fn, { defer: true })` replacement;
331
334
  the effect first runs on the next change.
332
335
 
336
+ When the side effect should fire **once per arming** rather than on every change —
337
+ "run this the first time the value crosses the threshold" — use `createReaction(fn)`
338
+ from `solid-js`: it returns a tracker; `fn` runs once after a source read inside a
339
+ `track(() => …)` call changes, then stays silent until the tracker is called again
340
+ (re-arm from inside `fn` to keep listening). Nothing runs at creation, and changes
341
+ made while unarmed are ignored. <!-- upstream:create-reaction -->
342
+
333
343
  A two-phase effect whose apply only calls a local signal setter is "state + effect" in
334
344
  disguise — formally legal Solid 2, still rule 4 (`solid2-kit check` flags it). The reset
335
345
  form of a writable derivation replaces it, and moves the source read to the read site
@@ -452,7 +462,9 @@ return <StoryDetail story={story} />;
452
462
  `affects(item, "quantity")`) or `isPending(() => todos.length)` dims only what
453
463
  that read depends on. A hold with no feedback at all is `[SILENT_HOLD]` under
454
464
  attribution — always pair a held write with one of these questions, an
455
- optimistic value, or `affects()`.
465
+ optimistic value, or `affects()`. `affects` keys address store leaves only:
466
+ `affects(accessor, "quantity")` throws `[INVALID_AFFECTS_TARGET]` in dev
467
+ <!-- upstream:invalid-affects-target --> — the keyed form needs a store (or store row); a bare accessor takes no key.
456
468
  - Default navigation **holds**. `selectedId()` and the old story stay put until
457
469
  the new answer lands, so the highlight never points at the wrong content.
458
470
  `latest(selectedId)` is the opt-in when the design wants the highlight to move
@@ -460,14 +472,27 @@ return <StoryDetail story={story} />;
460
472
  `transition-delay` so fast swaps do not flash. Inputs that feed a request follow
461
473
  the same rule: bind `value={latest(query)}` so a controlled search box reflects
462
474
  keystrokes while its write is held (an uncontrolled input needs nothing).
475
+ When one write in a handler must *not* join the hold — closing a menu before
476
+ `props.onSelect` starts a navigation — defer just that write with
477
+ `onFlush(() => setOpen(false))` (since 2.0.0-rc.19): it runs once right after the
478
+ pending flush as its own update, instead of flushing early
479
+ <!-- upstream:on-flush -->. It belongs in event handlers and effect callbacks; in a
480
+ component body or computation dev warns `ON_FLUSH_IN_OWNED_SCOPE`.
463
481
  - Read every reactive input **before the first `await`** in an async computation. A read
464
482
  after `await` does not subscribe (dev reports `[UNTRACKED_READ_AFTER_AWAIT]` on V8 —
465
483
  Chromium, Node, Deno, Bun); production can sit pending with no retry.
484
+ - An async memo returns **data**, never rendered output. On the server, a memo (or async
485
+ iterable source) landing on JSX, a component result, or a `<Loading>`/`<Errored>`
486
+ boundary cannot serialize for hydration: since 2.0.0-rc.19 the memo rejects with
487
+ `ASYNC_MEMO_ELEMENT` and the nearest `<Errored>` contains it like any render error
488
+ <!-- upstream:async-memo-element -->. Render the JSX from the data the memo returns.
466
489
  - `<Errored>` function fallbacks receive an error **accessor** and a `reset` callback:
467
490
  `fallback={(error, reset) => ...}`. The boundary **heals** when the source succeeds
468
491
  again (refresh, live reconnect, input change) — it is graph status, not a terminal
469
492
  React ErrorBoundary. `reset` retries the collected *data sources*, not a UI remount.
470
- - Refetch: `refresh(results)`. In-flight indicator: `isPending(results)` (or
493
+ - Refetch: `refresh(results)`. `refresh` takes a source accessor or a refreshable
494
+ store only — anything else (a plain function included) throws `[INVALID_REFRESH_TARGET]`
495
+ in dev <!-- upstream:invalid-refresh-target -->, and prod silently accepts it. In-flight indicator: `isPending(results)` (or
471
496
  `isPending(() => results())`). Freshest in-flight value for a preview:
472
497
  `latest(results)`. `latest` is not a null-safe probe: before the source's first value
473
498
  it throws `NotReadyError` in every scope (since 2.0.0-rc.9 also in event handlers and
@@ -540,7 +565,7 @@ functions (middleware, request setup) reported via
540
565
  `renderToString` / `renderToStream`'s first pass is reported `render` / `failed`
541
566
  before rethrow. <!-- upstream:report-request-failure --> A monitoring SDK's `init()` that
542
567
  registers this hook must load before the server graph — put it in the plugin's
543
- `start: { instrument: "./src/instrument.ts" }`, not at the top of an entry (see
568
+ `start: { serverSetup: "./src/instrument.ts" }`, not at the top of an entry (see
544
569
  [Production observability](#production-observability-the-observe-build)).
545
570
 
546
571
  `render` / `hydrate` accept a per-root `onError` that wins over the ambient hook —
@@ -558,7 +583,15 @@ errors matter (the rc.8 one-argument form keeps working). <!-- upstream:render-o
558
583
  Push-based sources integrate by returning an **async iterable** from a computation — reads
559
584
  then suspend to `<Loading>` until the first value, later pushes update the settled value,
560
585
  and thrown errors reach `<Errored>`. Integration layers should return a plain accessor, not
561
- `{ data, error }` signals:
586
+ `{ data, error }` signals.
587
+
588
+ For a callback subscription, an Observable, or an existing AsyncIterable, the adapter is
589
+ built in: `from(source)` returns the async iterable a computation consumes —
590
+ `createMemo(() => from(source))` <!-- upstream:from-source-adapts -->. Read the inputs that
591
+ choose the source before calling it, so a change re-runs the memo, closes the old source,
592
+ and opens the new one <!-- upstream:from-source-adapts -->. The hand-rolled form below remains for what `from` does not fit —
593
+ an event stream where every message matters (keep a queue instead of conflating to the
594
+ latest), or a subscribe signature that is not `(next, error, complete) => unsubscribe`:
562
595
 
563
596
  ```tsx
564
597
  import { createMemo, onCleanup } from 'solid-js';
@@ -736,7 +769,8 @@ list). An in-flight action does not freeze unrelated writes. Collecting failed
736
769
  actions to replay is a design choice, not something the runtime requires.
737
770
 
738
771
  Ordinary signal/store writes inside an action are held until it settles. Never call
739
- `flush()` inside an action. Invoke actions from handlers, not from component/computation bodies.
772
+ `flush()` inside an action. Invoke actions from handlers, not from component/computation bodies (dev warns
773
+ `[ACTION_CALLED_IN_OWNED_SCOPE]` <!-- upstream:action-in-owned-scope -->).
740
774
  Do not set a `submitted` signal and watch it from an effect — that work belongs in the
741
775
  handler or the `action`.
742
776
 
@@ -748,7 +782,9 @@ leaves an empty list once the action settles, with no warning. <!-- upstream:opt
748
782
  from the function form — `createOptimisticStore(() => api.list(), [])` refreshed
749
783
  after the `yield`, or `createOptimisticStore(() => base.todos, [])` over a separate
750
784
  `createStore` that the action writes the confirmed value into. Keep the value form for
751
- in-flight flags.
785
+ in-flight flags. A write against a function-form source whose first answer has not
786
+ landed yet is not guessed at all — dev warns `[OPTIMISTIC_WRITE_BEFORE_LOAD]`
787
+ <!-- upstream:optimistic-write-before-load -->; let the first answer land before writing.
752
788
 
753
789
  `createOptimistic` / `createOptimisticStore` are for a tentative value during an
754
790
  active mutation, not a local editing session (use a writable derivation or a plain
@@ -1073,7 +1109,11 @@ do not `return () => cleanup`. Put owned setup in a **directive factory** (like
1073
1109
  `onCleanup` throws there (`[CLEANUP_IN_FORBIDDEN_SCOPE]`), creating a memo or effect
1074
1110
  throws (`[PRIMITIVE_IN_FORBIDDEN_SCOPE]`), and so does `flush()` — each halts the
1075
1111
  reactive system in dev. Return the cleanup; create effects and memos in the component
1076
- body, next to the `onSettled` call, not inside it.
1112
+ body, next to the `onSettled` call, not inside it. A pending async read inside the
1113
+ callback warns `[PENDING_ASYNC_FORBIDDEN_SCOPE]` <!-- upstream:pending-async-forbidden-scope --> — the callback is not a
1114
+ computation, so nothing re-runs it when the answer lands. Register `onSettled` in the
1115
+ body, not out-of-band after an `await`: a cleanup returned from an unowned registration
1116
+ warns `[SETTLED_CLEANUP_UNOWNED]` <!-- upstream:settled-cleanup-unowned --> and never runs.
1077
1117
 
1078
1118
  ```tsx
1079
1119
  // WRONG — the mechanical 1.x port of onMount + onCleanup
@@ -1098,6 +1138,12 @@ component body or the compute phase, or attach them to an owner captured with
1098
1138
  `getOwner()` in the body via `runWithOwner(owner, …)`.
1099
1139
  <!-- upstream:primitive-in-effect-callback -->
1100
1140
 
1141
+ `createOwner` is not public API: since rc.21 it is typed only through
1142
+ `solid-js/internal`, as a runtime export kept for the renderers
1143
+ <!-- upstream:create-owner-internal -->. Host a scope with `createRoot()` and pass its `dispose` argument
1144
+ around as a plain function (a detached `owner.dispose` is a method and throws unbound
1145
+ <!-- upstream:ownerless-and-disposed -->); re-enter the scope with `runWithOwner(owner, …)`.
1146
+
1101
1147
  ### Per-value SSR policy: `ssrSource`
1102
1148
 
1103
1149
  `clientOnly` splits a *component*; `ssrSource` keeps one *value* off the server,
@@ -1227,13 +1273,23 @@ Always-on dev checks include misplaced reads
1227
1273
  for a first read made after an `await` in an async compute — V8 engines only), misplaced writes
1228
1274
  (`[REACTIVE_WRITE_IN_OWNED_SCOPE]` — since rc.9 also for store setters called in a
1229
1275
  component or root body <!-- upstream:store-write-owned-scope -->, `[ASYNC_STORE_SETTER]` for a store setter callback that returns
1230
- a promise <!-- upstream:async-store-setter-throws -->, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`,
1276
+ a promise <!-- upstream:async-store-setter-throws -->, `[FLUSH_IN_ACTION]`,
1277
+ `[ACTION_CALLED_IN_OWNED_SCOPE]` for an `action()` invoked from a component body or
1278
+ computation <!-- upstream:action-in-owned-scope -->, `[SERVER_WRITE]`,
1231
1279
  and since rc.17 `[STORE_WRITE_OUTSIDE_SETTER]` for a write to the store proxy itself
1232
1280
  <!-- upstream:store-write-outside-setter -->), misplaced bindings
1233
1281
  (`[UNCOMMITTED_INPUT_OVERWRITTEN]` — a `value` / `checked` binding re-applied its
1234
1282
  committed value over uncommitted user input <!-- upstream:uncommitted-input-overwritten -->),
1235
- `[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it, and
1236
- `[LOADING_ON_OUTSIDE_HOLD]` when a `<Loading on>` boundary's data is also read outside it.
1283
+ `[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it,
1284
+ `[LOADING_ON_OUTSIDE_HOLD]` when a `<Loading on>` boundary's data is also read outside it,
1285
+ `[LOADING_ON_ACCESSOR]` when a `<Loading on>` receives a function instead of the value
1286
+ (the boundary never calls it, so it never re-arms; since rc.20) <!-- upstream:loading-on-accessor -->, and
1287
+ since rc.21 `[CAUGHT_REACTIVE_READ]` when a computation catches a reactive read's throw
1288
+ itself and completes with a value — the catch masks the load or hides the error from the
1289
+ boundaries <!-- upstream:caught-reactive-read -->. Ownerless creation warns rather than throws: `[NO_OWNER_EFFECT]`
1290
+ for an effect created outside any root or component, `[NO_OWNER_BOUNDARY]` for a
1291
+ boundary invoked outside a reactive context, and `[RUN_WITH_DISPOSED_OWNER]` for
1292
+ `runWithOwner` on a disposed owner <!-- upstream:ownerless-and-disposed --> — none of them is ever disposed.
1237
1293
  Each entry carries a stable `[CODE]`, what the runtime observed, an owner chain
1238
1294
  (`in <App> › <Cart> › <LineItem>`), and — since rc.16, under `vite dev` — an
1239
1295
  `at src/Cart.tsx:12:5` line naming where the scope was created (the compiler's
@@ -1325,7 +1381,8 @@ first), `[ABANDONED_FLIGHTS]` (a source re-asked on every keystroke, answers thr
1325
1381
  — debounce at the handler), `[STACKED_HOLDS]` (repeated clicks piling into one hold),
1326
1382
  `[FALLBACK_FLASH]` (a fallback shown under 150ms), `[WASTED_RECOMPUTE]` (a scope whose
1327
1383
  runs mostly compared equal — narrow its reads upstream), `[GRAPH_GROWTH]` (the graph grows
1328
- on every visit to a route — an undisposed root or subscription), and `[OPTIMISTIC_REVERTED]`
1384
+ on every visit to a route — an undisposed root or subscription), `[HOT_SCOPE_FANOUT]`
1385
+ (many scopes hot from one root cause), and `[OPTIMISTIC_REVERTED]`
1329
1386
  (info: a guess the screen showed was replaced). `costs()` and `feedback()` (named imports)
1330
1387
  rank the session worst-first; `attribution.history("hold")` / `("waterfall")` /
1331
1388
  `("navigation")` / `("interaction")` hold the fact tables behind the verdicts (each hold
@@ -1546,10 +1603,15 @@ production too (observe build; a no-op on the prod build).
1546
1603
  - **Instrumentation order.** A monitoring SDK's `init()` (and `configureServerErrors`)
1547
1604
  must run before the modules it patches load. `import "./instrument"` at the top of an
1548
1605
  entry does not achieve that in ESM (static imports are hoisted in dependency order);
1549
- the plugin's `start: { instrument: "./src/instrument.ts" }` awaits that server-only
1550
- module before anything else in the server graph — since vite-plugin 3.0.0-next.51 the
1551
- `node` entry too: `dist/server/node.js` awaits `./server.js` (which awaits the
1552
- instrument module) and then loads `node-runtime.js`, so deploy both files. Failures the
1606
+ the plugin's `start: { serverSetup: "./src/instrument.ts" }` awaits that server-only
1607
+ module before anything else in the server graph — the `node` entry too:
1608
+ `dist/server/node.js` awaits `./server.js` (which awaits the
1609
+ setup module) and then loads `node-runtime.js`, so deploy both files. The option was
1610
+ renamed from `start.instrument` between vite-plugin next.52 and next.57 — the plugin
1611
+ throws on the old key <!-- upstream:start-server-setup -->. The sibling seam
1612
+ `start: { serverRenderSetup: "./src/render-setup.ts" }` runs per request immediately
1613
+ before SSR — `(event, App) => Component | void` — for integrations that must prepare
1614
+ a per-request app instance before the render. Failures the
1553
1615
  handler contains itself reach the hook as `{ kind: "request", handling: "failed" }`.
1554
1616
  The client-side counterpart is `start: { clientSetup: "./src/client-setup.ts" }`: a
1555
1617
  browser module run (and awaited, top-level `await` allowed) before `hydrate` / `render`,
@@ -1681,7 +1743,10 @@ plain `<a href={Router.paths.about}>` (or `useNavigate`); there is no `<A>` /
1681
1743
  `<Navigate>` / `<HashRouter>` / `<FileRoutes />`. Do not assign
1682
1744
  `window.location.href` or call `history.pushState` for in-app navigation.
1683
1745
  Session location is `useLocation` / `useParams`,
1684
- not `Router.paths`.
1746
+ not `Router.paths`. `useMatch(() => "/docs/:page")` tests a pattern against the
1747
+ current pathname without a route existing for it — the memo answers the match (params
1748
+ typed from the pattern) or nothing; that is the breadcrumb / section-highlight read
1749
+ <!-- upstream:router-use-match -->.
1685
1750
 
1686
1751
  **Route `preload` starts reads; the page reads them in a memo.** `preload` runs when
1687
1752
  the route matches (and ahead of a click under a `preloadLinks` strategy <!-- upstream:preload-links-opt-in -->), so it should kick off the query and return
@@ -2033,9 +2098,13 @@ every POST call as one). `live(GET(fn))` is the same declaration spelled out —
2033
2098
  component rendered into a document that reads an *undeclared* async iterable still
2034
2099
  producing after 5s is `[SSR_UNDECLARED_LIVE_SOURCE]` in dev — declare it `live()` (the
2035
2100
  document takes the first value and closes it; the client connects after hydration) or
2036
- bound the source. `broadcast(fn)` (experimental) is `live()` for one answer shared across
2101
+ bound the source. `broadcast(fn)` is `live()` for one answer shared across
2037
2102
  every viewer — the server runs the generator once per argument set instead of once per
2038
- connection.
2103
+ connection. In a router app, `liveQuery(fn, "key")` from `@solidjs/router` declares
2104
+ the `live()` server function itself and keeps one connection per key, shared by every
2105
+ consumer — when several components need the same stream, declare it once with
2106
+ `liveQuery` instead of hoisting a hand-rolled `live()` declaration and sharing code
2107
+ <!-- upstream:router-live-query -->.
2039
2108
 
2040
2109
  ```ts
2041
2110
  // src/data/auction.ts
@@ -2171,7 +2240,7 @@ how to reuse. Prefer the form on the right.
2171
2240
  | `createEffect(() => user, (u) => { log(u.name); })` | `createEffect(() => user.name, (name) => { log(name); })` |
2172
2241
  | `createEffect(() => title(), (t) => (document.title = t))` / `(v) => setX(v)` | `(t) => { document.title = t; }` — apply returns a cleanup or nothing; any other return value halts reactivity. An apply that only calls a local setter is rule 4 even with braces (`solid2-kit check` flags it): use a (writable) derivation |
2173
2242
  | `<Loading fallback={<PageSkeleton />}>{/* header + data */}</Loading>` | wrap only the data slot; chrome stays outside |
2174
- | `<Loading on={id} fallback={...}>` (the accessor), or `` on={`${a()}/${b()}`} `` to key several inputs | `on={id()}` / `on={[a(), b()]}` — `on` is a tracked dependency list, not a compared key: a bare accessor is never read (`solid2-kit check` flags it), and an array is the several-inputs form <!-- upstream:loading-on-dependency-list --> |
2243
+ | `<Loading on={id} fallback={...}>` (the accessor), or `` on={`${a()}/${b()}`} `` to key several inputs | `on={id()}` / `on={[a(), b()]}` — `on` is a tracked dependency list, not a compared key: a bare accessor is never read (`solid2-kit check` flags it; dev warns `[LOADING_ON_ACCESSOR]` since rc.20 <!-- upstream:loading-on-accessor -->), and an array is the several-inputs form <!-- upstream:loading-on-dependency-list --> |
2175
2244
  | `setCount(count() + 1)` when writes can batch | `setCount((c) => c + 1)` |
2176
2245
  | `setHandler(fn)` to store a function | `setHandler(() => fn)` (otherwise `fn` is an updater) |
2177
2246
  | `{user() ? <P user={user()!} /> : <SignIn />}` | `<Show when={user()}>{(u) => <P user={u()} />}</Show>` |
@@ -2279,7 +2348,7 @@ how to reuse. Prefer the form on the right.
2279
2348
  | `attribution.enable()` unguarded in app code, or `enable()` with the default `log: true` left on | `if (isDev) attribution.enable({ log: false })` — on the observe build an unguarded `enable()` records in production; prefer the `/__solid/diagnostics` session (`begin` enables, `end` disables) so app code carries nothing |
2280
2349
  | `solid({ observe: true })` added "to see diagnostics in dev" | `vite dev` is always the dev build; `observe: true` is the production-observability opt-in (observe condition + `sourceNames` for `vite build`/preview <!-- upstream:source-names-option -->) |
2281
2350
  | `configureServerErrors({ onError: (e) => { report(e); return e; } })` | return nothing or a reference (`new Error(\`ref ${id}\`)`) — the return value replaces the sanitized wire value; the error itself leaks message/stack/secrets |
2282
- | `import "./instrument"` at the top of the server entry for a monitoring SDK | `start: { instrument: "./src/instrument.ts" }` — ESM hoists static imports in dependency order, so the entry's own deps load first (next.51 wraps the `node` entry too: deploy `node.js` with `server.js`). Browser-side init before `hydrate` / `render` is `start: { clientSetup: "./src/client-setup.ts" }` |
2351
+ | `import "./instrument"` at the top of the server entry for a monitoring SDK | `start: { serverSetup: "./src/instrument.ts" }` — ESM hoists static imports in dependency order, so the entry's own deps load first (the `node` entry wraps it too: deploy `node.js` with `server.js`; the pre-rename spelling `start.instrument` throws in the plugin). Browser-side init before `hydrate` / `render` is `start: { clientSetup: "./src/client-setup.ts" }` |
2283
2352
  | Express/middleware injecting `traceparent` / trace headers into responses | the runtime carries a sampled or provided trace on `Server-Timing` + `<meta>`; forward downstream with `getTraceContext()?.entries`; SDKs use `OBSERVE?.server.trace.provide` |
2284
2353
  | `OBSERVE.records.subscribe("call", (e) => setStats(...))` | listeners run synchronously inside the runtime and must not write signals; collect into a plain buffer and drain from outside |
2285
2354
  | `import { storePath } from "solid-js"` | gone from `solid-js` (rc.9); write draft setters `setState((d) => { d.user.name = "Grace"; })` <!-- upstream:store-path-removed --> |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.23.1",
3
+ "version": "0.25.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",