diff --git a/scripts/build-llms.mjs b/scripts/build-llms.mjs index de599fe7a..e7b89542a 100644 --- a/scripts/build-llms.mjs +++ b/scripts/build-llms.mjs @@ -2,6 +2,8 @@ import { readFile, writeFile } from "node:fs/promises"; import { dirname, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; +import { API_SECTIONS } from "./llms-small/api.mjs"; + const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const OUTPUT_DIR = resolve(ROOT, process.argv[2] ?? "dist"); const SITE_ORIGIN = new URL( @@ -11,6 +13,8 @@ const SITE_ORIGIN = new URL( ); const INDEX_PATH = resolve(OUTPUT_DIR, "llms.txt"); const FULL_PATH = resolve(OUTPUT_DIR, "llms-full.txt"); +const SMALL_PATH = resolve(OUTPUT_DIR, "llms-small.txt"); +const PRIMER_PATH = resolve(ROOT, "scripts/llms-small/primer.md"); const DOCUMENT_LINK = /^- \[([^\]]+)\]\(([^)]+\.md)\)(?:: .*)?$/gm; @@ -74,13 +78,21 @@ function curateIndex(index) { } const fullCorpusUrl = new URL("/llms-full.txt", SITE_ORIGIN).toString(); - if (!lines.some((line) => line.includes(fullCorpusUrl))) { - lines.splice( - firstSectionIndex, - 0, + const smallCorpusUrl = new URL("/llms-small.txt", SITE_ORIGIN).toString(); + const pointers = [ + [ + fullCorpusUrl, `For a single-file corpus, use [llms-full.txt](${fullCorpusUrl}).`, - "" - ); + ], + [ + smallCorpusUrl, + `For a short primer with one line per solid-js export, use [llms-small.txt](${smallCorpusUrl}).`, + ], + ] + .filter(([url]) => !lines.some((line) => line.includes(url))) + .map(([, pointer]) => pointer); + if (pointers.length > 0) { + lines.splice(firstSectionIndex, 0, ...pointers, ""); } return `${absolutizeMarkdownLinks(lines.join("\n"))}\n`; @@ -114,6 +126,69 @@ async function buildFullCorpus(title, documents) { ].join("\n"); } +// The primer source starts with an HTML comment naming where it was copied +// from; the comment is for editors of this repository, not for the file. +function stripLeadingComment(markdown) { + return markdown.replace(/^\s*\s*/, ""); +} + +// Prettier pads Markdown table cells to the widest row; the padding is a +// third of the primer's tables, and llms-small.txt is read by tools, not +// people, so the cells shrink to one space on each side. +function compactTables(markdown) { + return markdown + .split("\n") + .map((line) => + line.startsWith("|") + ? line.replace(/-{3,}/g, "---").replace(/ {2,}/g, " ") + : line + ) + .join("\n"); +} + +function renderApiTable() { + return API_SECTIONS.map((section) => + [ + `### ${section.title}`, + "", + "| Export | Purpose |", + "| --- | --- |", + ...section.exports.map( + ([name, purpose]) => `| \`${name}\` | ${purpose} |` + ), + ].join("\n") + ).join("\n\n"); +} + +async function buildSmallCorpus(title) { + const primer = stripLeadingComment(await readFile(PRIMER_PATH, "utf8")) + .trim() + .split("\n"); + if (!primer[0]?.startsWith("# ")) { + throw new Error("The primer must start with a level-one heading"); + } + const primerTitle = primer[0].slice(2).trim(); + const primerBody = compactTables(primer.slice(1).join("\n").trim()); + + return [ + `# ${title}: ${primerTitle}`, + "", + "> The short form for tools with a small context budget: the rules that differ from React and Solid 1.x, the 1.x to 2.0 corrections, and one line per `solid-js` export.", + "", + `Index: ${new URL("/llms.txt", SITE_ORIGIN)}`, + `Full corpus: ${new URL("/llms-full.txt", SITE_ORIGIN)}`, + "", + primerBody, + "", + "## solid-js exports", + "", + "One line per public export of `solid-js`; the reference pages in the index carry the signatures.", + "", + renderApiTable(), + "", + ].join("\n"); +} + const generatedIndex = await readFile(INDEX_PATH, "utf8"); const documents = parseDocuments(generatedIndex); if (documents.length === 0) { @@ -125,16 +200,18 @@ if (!title) { throw new Error("llms.txt does not contain a title"); } -const [curatedIndex, fullCorpus] = await Promise.all([ +const [curatedIndex, fullCorpus, smallCorpus] = await Promise.all([ Promise.resolve(curateIndex(generatedIndex)), buildFullCorpus(title, documents), + buildSmallCorpus(title), ]); await Promise.all([ writeFile(INDEX_PATH, curatedIndex, "utf8"), writeFile(FULL_PATH, fullCorpus, "utf8"), + writeFile(SMALL_PATH, smallCorpus, "utf8"), ]); console.log( - `Generated llms.txt and llms-full.txt from ${documents.length} documents.` + `Generated llms.txt and llms-full.txt from ${documents.length} documents, and llms-small.txt from the primer and ${API_SECTIONS.reduce((n, section) => n + section.exports.length, 0)} exports.` ); diff --git a/scripts/llms-small/api.mjs b/scripts/llms-small/api.mjs new file mode 100644 index 000000000..415cb47f6 --- /dev/null +++ b/scripts/llms-small/api.mjs @@ -0,0 +1,257 @@ +// One line per public runtime export of `solid-js`, for llms-small.txt. +// Names follow packages/solid/src/index.ts in the solidjs/solid repository; +// purposes follow packages/solid/CHEATSHEET.md and the reference pages under +// src/routes/reference/(1)solid-js. Exports marked `@internal` upstream or +// hidden by scripts/extract-solid-ref.mjs (compiler and renderer ABI such as +// `createComponent`, `createOwner`, `enableHydration`, `$PROXY`) stay out. +// When an export is added or removed upstream, change this list and the +// reference pages together. + +export const API_SECTIONS = [ + { + title: "Signals and memos", + exports: [ + [ + "createSignal", + "`[get, set]` reactive value; `createSignal(fn)` is a writable derived value", + ], + [ + "createMemo", + "read-only derived value; a returned Promise is pending until it lands", + ], + [ + "createDeferred", + "async memo that keeps its last value while a refetch is in flight", + ], + ["untrack", "run `fn` without subscribing to the reads inside it"], + ["flush", "apply queued writes now instead of on the next microtask"], + ["isEqual", "default equality of signals and memos"], + ], + }, + { + title: "Effects and lifecycle", + exports: [ + [ + "createEffect", + "`(compute, apply)`: compute tracks, apply runs effects and may return a cleanup", + ], + [ + "onSettled", + "run once after the current activity settles; return a cleanup (replaces `onMount`)", + ], + [ + "onCleanup", + "cleanup tied to a reactive run or owner disposal; for primitive authors", + ], + [ + "createRenderEffect", + "effect that runs during render, for DOM bindings; app code uses `createEffect`", + ], + [ + "createTrackedEffect", + "single-callback effect that tracks and applies in one scope; rare", + ], + [ + "createReaction", + "one-shot tracked callback: `track(() => dep())` runs `fn` on the next change", + ], + ], + }, + { + title: "Stores", + exports: [ + [ + "createStore", + "deep reactive object; `set(draft => { ... })` writes through the draft", + ], + [ + "createProjection", + "read-only derived store; `{ key }` gives keyed reconciliation", + ], + [ + "reconcile", + "`reconcile(data, key)(draft.part)` merges new data into a store sub-tree by key", + ], + [ + "deep", + "track every nested level of a store (default tracking is per property)", + ], + [ + "snapshot", + "plain untracked copy of a store for JSON, storage and tests (replaces `unwrap`)", + ], + ["isWrappable", "whether a value would become a store proxy"], + [ + "merge", + "merge props-like objects into one reactive proxy (replaces `mergeProps`)", + ], + [ + "omit", + "reactive proxy of props without the listed keys (replaces `splitProps`)", + ], + ], + }, + { + title: "Async, actions and transitions", + exports: [ + [ + "action", + "generator mutation: optimistic writes, `yield` async work, `refresh`; reverts on failure", + ], + [ + "createOptimistic", + "signal whose writes inside an action show now and revert when it settles", + ], + ["createOptimisticStore", "store form of `createOptimistic`"], + [ + "isPending", + "true while a change to the values read inside `fn` is in flight", + ], + [ + "latest", + "read the in-flight value during a transition instead of the committed one", + ], + [ + "refresh", + "re-run one source after a server write; quiet unless `affects` was declared", + ], + [ + "affects", + "mark a source or store slot as pending while in-flight work will change it", + ], + [ + "until", + "Promise for the first time a reactive predicate turns truthy; `yield` it in an action", + ], + [ + "resolve", + "Promise for the first settled value of a reactive expression (tests)", + ], + [ + "NotReadyError", + "thrown by a read of an async source before its first value; boundaries handle it", + ], + ["TimeoutError", "rejection from `until` when its `timeout` passes"], + [ + "enableExternalSource", + "adapter so Solid tracks reads of another reactive system (MobX, Vue)", + ], + ], + }, + { + title: "Components and context", + exports: [ + ["children", "resolve `props.children` once; `.toArray()` helper"], + [ + "flatten", + "resolve nested accessors and arrays in a children tree (`children` memoizes it)", + ], + [ + "createContext", + "context for a subtree; `` is the provider; no default means no provider throws", + ], + ["useContext", "read the nearest context value"], + [ + "lazy", + "code-split component; pending through `` while its chunk loads", + ], + [ + "lazyModule", + "cached dynamic import of a non-component module; `.preload()` and `.peek()`", + ], + [ + "dynamicComponent", + "component that re-resolves `fn` to a component; the factory behind `dynamic`", + ], + [ + "createUniqueId", + "stable id that matches between server and client (`for`, `aria-labelledby`)", + ], + ], + }, + { + title: "Flow components", + exports: [ + [ + "For", + "keyed list; callback gets `(item, i())`, or `(item(), i)` with `keyed={false}`", + ], + ["Repeat", "render `count` times; `i` is a plain number"], + [ + "Show", + "render children when `when` is truthy; a function child gets a narrowed accessor", + ], + ["Switch", "render the first `` whose `when` is truthy"], + ["Match", "one branch of ``"], + [ + "Loading", + "async boundary (replaces `Suspense`); `on={deps}` re-arms it", + ], + [ + "Errored", + "error boundary (replaces `ErrorBoundary`); `fallback={(err, reset) => ...}`", + ], + [ + "Reveal", + "reveal order of sibling `` boundaries (replaces `SuspenseList`)", + ], + [ + "mapArray", + "reactive array map behind ``; reuses entries for unchanged items", + ], + ["repeat", "reactive count-based render behind ``"], + ], + }, + { + title: "Owners and roots", + exports: [ + [ + "createRoot", + "owned reactive scope with `dispose()`; tests and library entry points", + ], + [ + "getOwner", + "current owner, to capture across an async gap in library code", + ], + [ + "runWithOwner", + "run `fn` under an owner; `runWithOwner(null, fn)` detaches a root", + ], + ["getObserver", "the currently tracking computation, or `null`"], + ["isDisposed", "whether an owner has been disposed, to ignore late work"], + ], + }, + { + title: "Hydration (advanced)", + exports: [ + ["NoHydration", "opt a subtree out of client hydration"], + ["Hydration", "opt a subtree back in under ``"], + ["isHydrating", "whether code runs while claiming server-rendered DOM"], + [ + "isHydratable", + "whether the calling owner sits where hydration applies", + ], + [ + "onHydrationEnd", + "run once after the root pass and every streamed boundary have resumed", + ], + ], + }, + { + title: "Dev and observability", + exports: [ + [ + "DEV", + "dev tier: devtools hooks, graph traversal, diagnostics reporting; dev builds only", + ], + [ + "OBSERVE", + "observe tier: records channel, diagnostics channel, attribution slot", + ], + [ + "configureClientErrors", + "hook called once per error when a boundary renders its fallback", + ], + ], + }, +]; diff --git a/scripts/llms-small/primer.md b/scripts/llms-small/primer.md new file mode 100644 index 000000000..08c553a16 --- /dev/null +++ b/scripts/llms-small/primer.md @@ -0,0 +1,100 @@ + + +# Solid 2.0 for agents + +Solid is not React, and 2.0 is not 1.x. Pattern-matching from either is the +dominant bug source. Every name below is a `solid-js` export unless noted; +DOM APIs (`render`, `hydrate`, `Portal`, `dynamic`) are in `@solidjs/web`. + +## Three rules + +1. **Props are read, not destructured; pass values, not accessors.** + - Wrong: `function C({ name }) {}` and `` + - Right: `function C(props) { return

{props.name}

; }` and `` +2. **`createEffect` takes two functions: compute (tracks), then apply (side effects).** + - Wrong: `createEffect(() => { el.title = count(); });` + - Right: `createEffect(() => count(), v => { el.title = v; });` +3. **Stores are written through the draft.** + - Wrong: `setStore("user", "name", "B")` or `setStore(produce(s => ...))` + - Right: `setStore(s => { s.user.name = "B"; });` + +## 1.x → 2.0 corrections + +Nothing in the left column exists in 2.0. + +| Wrong (1.x) | Right (2.0) | +| ---------------------------------------------- | ----------------------------------------------------------------- | +| `import { createStore } from "solid-js/store"` | `import { createStore } from "solid-js"` | +| `import { render } from "solid-js/web"` | `import { render } from "@solidjs/web"` | +| `"jsxImportSource": "solid-js"` | `"jsxImportSource": "@solidjs/web"` | +| `createEffect(fn)` (one argument) | `createEffect(compute, apply)` | +| `onMount(fn)` | `onSettled(() => { setup(); return cleanup; })` | +| `createEffect(on(dep, fn))` | `createEffect(() => dep(), v => fn(v))` | +| `const [d] = createResource(id, fetchX)` | `const d = createMemo(() => fetchX(id()))` under `` | +| `d.loading` / `d.error` | `` or `isPending(() => d())` / `` | +| `refetch()` | `refresh(d)` (`affects(d); refresh(d)` to read as pending) | +| `` / `` | `` / `` | +| `` | ` ...}>` (heals automatically) | +| `` | `` | +| `batch(fn)` | not needed: updates batch per microtask; `flush()` applies now | +| `startTransition` / `useTransition` | built-in transitions: `action(...)`, `isPending`, optimistic APIs | +| `createComputed` | `createMemo`, split `createEffect`, or `createSignal(fn)` | +| `mergeProps` / `splitProps` / `unwrap` | `merge` / `omit` / `snapshot` | +| `produce`, `createMutable`, `modifyMutable` | draft setters are the default in `createStore` | +| `createSelector` | `createProjection` (or `createStore(fn)`) | +| `` | `` | +| `classList={{ a: x() }}` | `class={{ a: x() }}` or `class={["base", { a: x() }]}` | +| `use:dir={x}` / `attr:` / `on:` / `tabIndex` | `ref={dir(x)}` / plain attributes / `onClick` / `tabindex` | +| `onError` / `catchError` | `` or `createEffect(c, { effect, error })` | +| `from(source)` / `observable` | return an AsyncIterable from a computation | + +Also: + +- Reads lag writes: `setX(1); x()` still returns the old value until the + microtask or `flush()`. In tests, `flush()` before asserting. +- No writes inside a memo, effect compute, or component body (dev throws). + Write from event handlers, `onSettled`, or actions. +- No reactive reads at the top of a component body; read in JSX, a memo, or + `untrack`. Flow callbacks (``, ``, ``) are owners, not + tracking scopes: read their accessor arguments in JSX, not at the top. +- ``: default gives a raw item and an index accessor; `keyed={false}` + gives an item accessor and a plain number. ``: `i` is a number. +- `onClick={cond() ? a : b}` binds once; decide inside a stable handler. + +## Primitives to reach for + +| Primitive | Use | +| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | +| `createSignal(0)` / `createSignal(() => fetchUser(id()))` | plain signal / writable derived value, sync or async; read by calling it | +| `createMemo(() => fetchX(id()))` | read-only derived value; a Promise result is pending until it lands | +| `createProjection(async () => api.list(), [], { key: "id" })` | read-only derived store with keyed reconciliation | +| `createOptimisticStore(() => api.list(), [])` | store whose optimistic writes revert when the transition settles | +| `action(function* (x) { write(); yield api.put(x); refresh(d); })` | mutation with transition coordination; optimistic writes go inside | +| `until(() => confirmed(), { timeout })` | Promise that resolves when a predicate turns truthy; `yield` it in an action | +| `isPending(() => d())` | true while a change to `d` is in flight (input changed or `affects()` declared) | +| `latest(d)` | peek at the in-flight value during a transition | +| `refresh(d)` | re-ask the source after a server write; quiet unless preceded by `affects(d)` | +| `` `` `` `` `` | async boundary (`on` re-arms it), error boundary, sibling order, range list, keyed list | + +## Pointers + +- Full reference: `node_modules/solid-js/CHEATSHEET.md`; the same text is + the `solid-2.0-cheatsheet` skill in `node_modules/solid-js/skills/`. +- Diagnostics: dev builds print `[CODE] message` plus an `in › …` + owner line. The first report of each code ends with a footer naming + `node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`, one section + per code (`STRICT_READ_UNTRACKED`, `REACTIVE_WRITE_IN_OWNED_SCOPE`, + `LOADING_ON_OUTSIDE_HOLD`, …). Fix the cause; never suppress a code. +- Evidence loops: `node_modules/@solidjs/diagnostics/skills/agent-loops/SKILL.md` + (`captureArtifact`). With `@solidjs/vite-plugin`'s `diagnostics` option the + dev server answers `POST /__solid/diagnostics` (`begin`, `active`, `whyDidRun`, + `costs`, `end`). +- Lint: `eslint-plugin-solid`, config `v2` + (`import solid from "eslint-plugin-solid/configs/v2"`) or `v2-strict`. +- Docs: https://v2.solidjs.com (Solid 2.0 docs; `/llms.txt` and + `/llms-full.txt` for agents). Migration guide: `documentation/solid-2.0/MIGRATION.md` + in the repository.