solid2-agent-kit 0.13.0 → 0.14.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.
|
@@ -34,6 +34,9 @@ Steps:
|
|
|
34
34
|
`[ASYNC_STORE_SETTER]`), store setters called at component-body top level, and
|
|
35
35
|
`latest()` used as a null-safe read of an unsettled source;
|
|
36
36
|
- nested fetches assumed to waterfall, or `<Loading>` lifted along with a lifted fetch;
|
|
37
|
+
- mutations on a value read through `live()` that settle on `yield save()` alone or
|
|
38
|
+
`refresh()` the live-derived store, instead of `yield until(predicate, { timeout })`
|
|
39
|
+
for the stream's echo (the old value flashes back when the overlay drops);
|
|
37
40
|
- rewriting App with loading/error branches, or snapshot/restore, when wrapping
|
|
38
41
|
a client store in server functions; disabling optimistic rows until ack;
|
|
39
42
|
- tRPC / type-gen around `"use server"`, a client `fetch` after a mutation,
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
- After editing TSX, run the `solid2-kit check` mechanical gate — it fails on React/Solid 1.x tokens and props destructuring. Use the project's `lint:solid` script if one exists; otherwise `npx solid2-agent-kit check` (with the kit as a devDependency) or `npx github:lightsound/solid2-agent-kit check`. An edit-time hook may also run this gate automatically after each file edit — treat its findings as errors to fix immediately, not warnings. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
|
|
6
6
|
- After editing `package.json`, `tsconfig*.json`, or root config files (Vite, ESLint), run `solid2-kit doctor` — it fails on React/Solid 1.x wiring: `react`/`vite-plugin-solid`/`eslint-plugin-solid`/SolidStart dependencies, 1.x `solid-js` ranges, and `jsx`/`jsxImportSource` values other than `preserve`/`@solidjs/web`.
|
|
7
7
|
- The kit's guidance and gate files (`.cursor/rules/solid-2.mdc`, the `solid-2` skill directories, hook entries, managed blocks in this file and `CLAUDE.md`) are guardrails, not obstacles. Never edit, weaken, bypass, or delete them to make a failure go away — fix the flagged code. If a guardrail seems wrong, stop and tell the user.
|
|
8
|
-
- Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`. One browser-only value takes `ssrSource` on the memo/signal/derived store (`client`/`hybrid`), not a `clientOnly` component split. Router link state is automatic (`aria-current`/`data-active`/`data-pending` + CSS; `useLinkState`/`useIsRouting` in JSX); search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`.
|
|
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`. 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(GET(fn))` (`live()` outermost). Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write), and a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`. Error monitoring is `configureClientErrors` / `configureServerErrors` (or `render(..., { onError })`), not a side effect inside `<Errored fallback>`; the server hook must return a reference or nothing, never the error itself. `<Dynamic>` is deprecated — use `dynamic()`. 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.
|
|
@@ -242,9 +242,16 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
242
242
|
`pending` on the record rather than a second copy of state. Do not snapshot
|
|
243
243
|
and restore, disable optimistic rows until ack, mutex rapid clicks, or freeze
|
|
244
244
|
unrelated writes because one action is in flight; failed-action replay is
|
|
245
|
-
optional.
|
|
246
|
-
{ timeout })` — the predicate reads authoritative state
|
|
247
|
-
|
|
245
|
+
optional. Confirmations that arrive on another channel wait with `yield until(predicate,
|
|
246
|
+
{ timeout })` — the predicate reads authoritative state, so the overlay cannot satisfy it.
|
|
247
|
+
That covers fire-and-forget sends **and** request/response mutations whose result is
|
|
248
|
+
read through a `live()` source: `live()` takes no part in revalidation or single-flight,
|
|
249
|
+
so after `yield save()` alone the overlay drops before the stream echoes the write and
|
|
250
|
+
the old value flashes back. Follow it with `yield until(...)`, not `refresh(liveStore)`
|
|
251
|
+
(that re-runs the derivation — the stream closes and the source reconnects — so each
|
|
252
|
+
mutation costs a reconnect, and the flash remains whenever the new connection's first
|
|
253
|
+
value predates the write). Skip `until` only for reactive clients (e.g. Convex)
|
|
254
|
+
whose subscriptions already carry the write when the mutation resolves. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
|
|
248
255
|
Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`) — run `solid2-kit doctor`
|
|
249
256
|
after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
|
|
250
257
|
wiring. Server vs browser:
|
|
@@ -346,7 +353,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
346
353
|
components from `"use server"` unless the project already enabled the
|
|
347
354
|
experimental `serverFunctions.components` flag. JSON-encodable server-function arguments
|
|
348
355
|
only, unless `enableRichArguments()` was called once in the client entry
|
|
349
|
-
(`Date` / `Map` / `Set` throw without it). `live()`
|
|
356
|
+
(`Date` / `Map` / `Set` throw without it). Declare live reads as `live(GET(fn))` —
|
|
357
|
+
`live()` outermost. A plain `async function*` server function is an event stream on
|
|
358
|
+
one connection (no reconnect when it drops); `live()` is one value that changes over
|
|
359
|
+
time and reconnects. `live()` connection state is
|
|
350
360
|
`source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), never a
|
|
351
361
|
field in the yielded value. Sibling `<Loading>` reveal order is `<Reveal>`
|
|
352
362
|
(`collapsed` suppresses tail skeletons under sequential order).
|
|
@@ -632,6 +632,38 @@ only the echo can. Timeout rejects with `TimeoutError` (strongly recommended on
|
|
|
632
632
|
droppable transports; abort via `signal`). Outside actions, `await until(() => user())`
|
|
633
633
|
waits for a value before continuing.
|
|
634
634
|
|
|
635
|
+
The same hold applies when the mutation *does* answer but the value is read through a
|
|
636
|
+
`live()` source. A live source updates only through its open stream — it takes no part
|
|
637
|
+
in revalidation or single-flight mutation data — so the mutation's response and the
|
|
638
|
+
stream's echo race. Settle on `yield saveBid(...)` alone and the overlay is discarded
|
|
639
|
+
first: the old bid flashes back until the stream catches up.
|
|
640
|
+
|
|
641
|
+
```tsx
|
|
642
|
+
import { action, createOptimisticStore, until } from 'solid-js';
|
|
643
|
+
import { liveAuction, saveBid } from '../data/auction';
|
|
644
|
+
|
|
645
|
+
function Auction(props: { id: string }) {
|
|
646
|
+
const [auction, setAuction] = createOptimisticStore(() => liveAuction(props.id), { highBid: 0 });
|
|
647
|
+
|
|
648
|
+
const bid = action(function* (amount: number) {
|
|
649
|
+
setAuction((a) => { a.highBid = amount; }); // overlay
|
|
650
|
+
yield saveBid(props.id, amount); // the write goes up
|
|
651
|
+
yield until(() => auction.highBid >= amount, { timeout: 10_000 }); // hold for the echo
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
return <button onClick={() => bid(auction.highBid + 1)}>Bid {auction.highBid + 1}</button>;
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Do not `refresh(auction)` in place of `until`: on a store derived from a live source it
|
|
659
|
+
re-runs the derivation — the current stream is closed and `liveAuction()` opens a new
|
|
660
|
+
connection. That costs a reconnect per mutation and hides the flash only when the new
|
|
661
|
+
connection's first value already includes the write; when the read side lags the write,
|
|
662
|
+
the old value still flashes back before the echo. Reactive clients whose
|
|
663
|
+
subscriptions already carry the write when the mutation resolves (e.g. Convex) need
|
|
664
|
+
neither. The server side of `liveAuction` / `saveBid` is in
|
|
665
|
+
[Server functions](#server-functions--use-server).
|
|
666
|
+
|
|
635
667
|
Navigation-shaped updates do **not** need core `action`. A plain setter is enough:
|
|
636
668
|
reads pull the async, and downstream async computeds hold previous values until the
|
|
637
669
|
new ones are ready (`isPending` / `latest`). Reach for `action` from `solid-js` only
|
|
@@ -1367,8 +1399,42 @@ export const fetchStory = GET(async (id: number) => {
|
|
|
1367
1399
|
});
|
|
1368
1400
|
```
|
|
1369
1401
|
|
|
1402
|
+
A value that changes over time is `live(GET(fn))` — **`live()` outermost**: `GET()`
|
|
1403
|
+
chooses the read transport and `live()` wraps the call behavior around it. `live()`
|
|
1404
|
+
does not imply `GET()`; without it the source streams over POST, which is fine when a
|
|
1405
|
+
cacheable URL is of no use.
|
|
1406
|
+
|
|
1407
|
+
```ts
|
|
1408
|
+
// src/data/auction.ts
|
|
1409
|
+
import { GET, live } from '@solidjs/web/server-functions';
|
|
1410
|
+
|
|
1411
|
+
export const liveAuction = live(
|
|
1412
|
+
GET(async function* (id: string) {
|
|
1413
|
+
'use server';
|
|
1414
|
+
yield await db.auctions.find(id); // current state first, from storage
|
|
1415
|
+
for await (const _change of db.auctions.subscribe(id)) {
|
|
1416
|
+
yield await db.auctions.find(id);
|
|
1417
|
+
}
|
|
1418
|
+
}),
|
|
1419
|
+
);
|
|
1420
|
+
|
|
1421
|
+
export async function saveBid(id: string, amount: number) {
|
|
1422
|
+
'use server';
|
|
1423
|
+
await db.bids.place(id, amount);
|
|
1424
|
+
}
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
A plain `async function*` server function (no `live()`) is a different tool: an
|
|
1428
|
+
**event sequence** on one connection, consumed with `for await` and accumulated. When
|
|
1429
|
+
that connection drops, the iteration ends with an error and nothing reopens it; ending
|
|
1430
|
+
the iteration in the browser aborts the request and fires the server's
|
|
1431
|
+
`request.signal`. Use it for an order's status history; use `live()` when each value
|
|
1432
|
+
replaces the previous one.
|
|
1433
|
+
|
|
1370
1434
|
`live()` yields **current state** (each yield replaces the last); do not treat
|
|
1371
|
-
it as an append-only event log.
|
|
1435
|
+
it as an append-only event log. A reconnect calls the function again and its first
|
|
1436
|
+
yield replaces the stale answer, so read that first value from the state mutations write
|
|
1437
|
+
to (database, shared server state) — not from generator locals or per-connection memory. Until the first yield it is unsettled
|
|
1372
1438
|
(`<Loading>`), same as a Promise; later yields are updates — the fallback does
|
|
1373
1439
|
not return.
|
|
1374
1440
|
Wire status is a side channel on the **iterable the `live()` call returns**,
|
|
@@ -1469,7 +1535,10 @@ how to reuse. Prefer the form on the right.
|
|
|
1469
1535
|
| `throw new Error("expired")` from a server function (prod) | `throw markSafeError(...)` or `throw respond(body, { status })` |
|
|
1470
1536
|
| `const r = await api.x(); if (!r.ok) ...` result-object checks at every read | throw unusable responses; contain each region with `<Errored>` |
|
|
1471
1537
|
| `GET(async (id) => { "use server"; await db.delete(id) })` | mutations stay on POST; `GET()` is for idempotent reads |
|
|
1472
|
-
| `live()` yields as an event log to append | each yield **replaces** the current answer; yield current state first |
|
|
1538
|
+
| `live()` yields as an event log to append | each yield **replaces** the current answer; yield current state first, read from the state mutations write to — a reconnect calls the function again, so generator locals or per-connection memory are not the source |
|
|
1539
|
+
| `GET(live(async function* () { ... }))` | `live(GET(async function* () { ... }))` — `live()` outermost; `live()` alone streams over POST |
|
|
1540
|
+
| a plain `async function*` `"use server"` stream for a value that must survive a dropped connection | `live()` — a plain stream is an event sequence on one connection (`for await`, accumulate); nothing reopens it when it drops |
|
|
1541
|
+
| `yield saveBid(...)` then `refresh(auction)` on a store derived from `live()` | `yield saveBid(...); yield until(() => auction.highBid >= amount, { timeout })` — `live()` is outside revalidation/single-flight; `refresh` re-runs the derivation (closes the stream, calls the source again): a reconnect per mutation, and the old value still flashes back whenever the new first value predates the write |
|
|
1473
1542
|
| `<article innerHTML={html()}>…children…</article>` | `innerHTML` **or** children, not both |
|
|
1474
1543
|
| `fallback={(error) => <p>{error.message}</p>}` | `error` is an accessor: `error().message` (or `String(error())`) |
|
|
1475
1544
|
| `import { action } from "solid-js"` on a `<form>` | `import { action } from "@solidjs/router"` + `<form action={save} method="post">` (core `action` is a generator transaction, not a form URL) |
|
|
@@ -1726,7 +1795,9 @@ always-applied rules installed alongside this skill.
|
|
|
1726
1795
|
setters + `action` + server functions): do not rewrite App with
|
|
1727
1796
|
loading/error branches, disable optimistic rows until ack, or refetch in
|
|
1728
1797
|
`hydrate` / `onSettled`. HTTP does not enforce TypeScript — validate
|
|
1729
|
-
inside `"use server"`; do not invent tRPC. `live()` owns the client connection
|
|
1798
|
+
inside `"use server"`; do not invent tRPC. `live()` owns the client connection,
|
|
1799
|
+
is declared `live(GET(fn))`, and a mutation on a live-read value holds with
|
|
1800
|
+
`yield until(...)` for the echo instead of `refresh()`.
|
|
1730
1801
|
A server bundle uses `handleRequest` from `dist/server/server.js`; client-only start
|
|
1731
1802
|
is static `dist/client`. Do not delay `renderToStream` for visual order
|
|
1732
1803
|
(`<Reveal>` does).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid2-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|