Skip to content

Commit 17d75a8

Browse files
committed
Update AGENTS.md for retarget
1 parent 837656a commit 17d75a8

1 file changed

Lines changed: 84 additions & 30 deletions

File tree

AGENTS.md

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
## Project overview
66

7-
The package `solid-querybuilder` is a Solid 1.x/2.x port of
7+
The package `solid-querybuilder` is a **Solid 2.0** port of
88
[React Query Builder](https://react-querybuilder.js.org), built on the published
99
`@react-querybuilder/core`. The port's defining constraint is **full DOM parity**: tag name,
1010
document order, `data-testid`, `data-path`, and byte-identical `class` attributes must match
@@ -19,11 +19,17 @@ solid-querybuilder/
1919
│ ├── src/ # components, reactive layer, types, styles
2020
│ ├── test/conformance/ # DOM-parity harness (fixtures gitignored)
2121
│ └── scripts/ # build/check/ssr-smoke helpers
22-
└── examples/ # demo (Vite) and a SolidStart SSR gate
22+
└── examples/ # demo (Vite) and an SSR gate
2323
```
2424

25+
**Target is Solid 2 only.** Peers are `solid-js@^2.0.0-beta.32` **and
26+
`@solidjs/web@^2.0.0-beta.32`** — in Solid 2 the DOM runtime is its own package. There is no
27+
`^1.9` leg anywhere: in the manifest, in CI, or in the source. A v1-target port, if it ever
28+
happens, is a separate repo publishing as `@react-querybuilder/solid1`; do not add compatibility
29+
shims or `solid-js@1` code paths here.
30+
2531
Wiring strategy is **hybrid**: `QueryManager` owns every write (history, guards, `reconfigure`);
26-
an internal `createStore` mirror (`reconcile`d by `id`) is the read path. See
32+
an internal store mirror (`reconcile`d by `id`) is the read path. See
2733
`~/git/SOLID_QB_PLAN.md` for the full rationale and step-by-step plan; this file only records the
2834
standing rules that apply to every step.
2935

@@ -34,9 +40,11 @@ standing rules that apply to every step.
3440
- `bun run test` / `bun run test:coverage` — Vitest. **Never `bun test`**; that is Bun's builtin
3541
runner and bypasses Vitest entirely.
3642
- `bun run conformance` — fetch fixtures, then run the DOM-parity suites
43+
- `bun run check:versions` — asserts the resolved prerelease toolchain has not drifted; runs
44+
first in CI
3745
- `bun run test:ssr` — resolves the `solid` export condition and renders through
38-
`renderToStringAsync`; a real gate from step 1, superseded (but not replaced) by the SolidStart
39-
gate at step 8
46+
`renderToString`; a real gate from step 1, superseded (but not replaced) by the SSR gate at
47+
step 8
4048
- `bun run check``tsc --noEmit`
4149
- `bun run lint`, `bun run fmt`, `bun run fmt:check`
4250
- `bun run check:all` — everything CI runs
@@ -59,22 +67,26 @@ A key _lookup_ (`exports['.'].solid`) is order-blind and does not gate anything
5967
regress it back to that.
6068

6169
Keep the script even after step 8 supersedes it with the SolidStart gate; it is the only thing
62-
that checks the export condition in isolation.
70+
that checks the export condition in isolation. (There is no Solid 2 SolidStart yet — step 8's
71+
gate is a plain Vite SSR example — but the export condition is what that gate rests on either
72+
way.)
6373

6474
### The SSR smoke test runs one Solid instance
6575

66-
`scripts/ssr-smoke-entry.jsx` imports **both** `solid-js/web` and the library, and is loaded
76+
`scripts/ssr-smoke-entry.jsx` imports **both** `@solidjs/web` and the library, and is loaded
6777
through `vite.ssrLoadModule`. That is load-bearing: `ssr.noExternal` gives Vite's module graph its
68-
own copy of `solid-js`, so importing `renderToStringAsync` in the host process instead would
69-
render with a _different instance_ than the component was compiled against. Solid keeps
78+
own copy of `solid-js`, so importing `renderToString` in the host process instead would render
79+
with a _different instance_ than the component was compiled against. Solid keeps
7080
owner/`sharedConfig` state at module scope, so the copies do not share it — a trivial component
7181
survives this, but anything using `createContext`/`createStore`/`createEffect` (i.e.
7282
`QueryBuilder`, from step 4) does not.
7383

74-
Vite's `ssr.resolve.conditions` must be `['solid', 'node', 'development']`: `solid` so the library
75-
resolves to its raw-JSX entry, **`node` so `solid-js/web` resolves to its server build**. Listing
76-
`solid` alone clobbers Vite's defaults and hands back the browser build, whose
77-
`renderToStringAsync` is a stub that throws. Never add `browser`.
84+
**Conditions are the plugin's job now, not the config's.** `vite-plugin-solid@3` gives the ssr
85+
environment `['solid', 'development', 'module', 'node', 'development|production']` on its own, so
86+
the script sets **no** `ssr.resolve.conditions`. Hand-maintaining a list on top of that only
87+
_removes_ entries. The failure it guards against is unchanged: `@solidjs/web`'s exports map lists
88+
`browser` **before** `node`, so any condition set carrying `browser` hands back the browser build,
89+
whose `renderToString` is a stub. Never add `browser`.
7890

7991
The entry is `.jsx`, not `.tsx`, deliberately: it stays out of the typecheck project so
8092
`bun run check` does not depend on `dist/` existing.
@@ -86,21 +98,53 @@ specifiers into the emitted `.d.ts` verbatim. `check-dist-specifiers.ts` additio
8698
`./foo.js` specifier in a `.d.ts` to resolve to a sibling `foo.d.ts` with no `foo.js` beside it (a
8799
type-only module erased by the bundler), and allows `./foo.jsx` under `dist/source`.
88100

89-
### Reactivity
90-
91-
- **Never destructure props.** `splitProps`/`mergeProps` only. A destructure at the top of a
92-
component silently severs reactivity and passes every type check. This is the single most likely
93-
Solid-specific defect class — check it at review of every component.
94-
- **`unwrap()` before handing anything to the manager.** The manager's Immer deep-freeze rejects a
95-
store proxy.
96-
- **`unwrap()` the manager itself** before reading its history (`UndoRedoActions`).
97-
`QueryManager` keeps history in private class fields, which a `Proxy` cannot read through.
98-
- Effects that write back into state use `createEffect(on([...explicit deps], ...))`, never a bare
99-
auto-tracking effect — the tracked set changing across branches is exactly the loop failure mode.
100-
Writes go through `untrack`, plus a re-entrancy flag.
101+
### Reactivity (Solid 2)
102+
103+
- **Never destructure props.** `merge`/`omit` only — Solid 2's replacements for
104+
`mergeProps`/`splitProps`. A destructure at the top of a component silently severs reactivity
105+
and passes every type check. This is the single most likely Solid-specific defect class — check
106+
it at review of every component.
107+
- ⚠️ **`merge` treats an explicit `undefined` as a real value** and overrides with it, where 1.x's
108+
`mergeProps` skipped it. Every `merge(defaults, props)` is therefore a latent defaults-erasure
109+
bug. A _missing_ key still falls through. Where "skip undefined" is wanted, filter explicitly,
110+
or prefer core's `preferProp`/`preferFlagProps`. `merge` is lazy (getters), not a snapshot.
111+
- **`snapshot()` — not `unwrap()` — before handing anything to the manager.** The manager's Immer
112+
deep-freeze rejects a store proxy. Likewise `snapshot()` the manager itself before reading its
113+
history (`UndoRedoActions`): `QueryManager` keeps history in private class fields, which a
114+
`Proxy` cannot read through.
115+
- **Split effects, not `on()`.** `on()` is gone; `createEffect(compute, apply)` makes the compute
116+
phase the dependency declaration, so the old "always use `on`" rule is now enforced by the API
117+
shape. Deps in compute, writes in apply. `{ defer: true }` survives as an option.
118+
- **Apply-phase writes are legal — no `ownedWrite` needed** (proven at step 1.5). The owned-write
119+
rule rejects writes made while an owner is on the stack, and the apply phase is unowned. Note
120+
`ownedWrite` is a **signal** option, not an effect option; `createEffect` has no such option.
121+
- ⚠️ **The body of `createRoot(fn)` IS an owned scope.** A write there throws
122+
`REACTIVE_WRITE_IN_OWNED_SCOPE`. Test harnesses must set up inside the root and write from
123+
outside it.
124+
- ⚠️ **A split effect's apply callback must return a cleanup function or `undefined`.** `v =>
125+
setX(v)` returns the setter's return value and throws "invalid cleanup value". Use a block body.
126+
- ⚠️ **An uncaught error inside an effect halts the entire reactive system** (`REACTIVITY_HALTED`)
127+
for the rest of the module. Intentionally-throwing tests need their own file.
128+
- **`batch` is gone; reads lag writes.** Not just effects — a plain `signal()` read after
129+
`setSignal()` still returns the _old_ value until the next microtask or an explicit `flush()`.
130+
Every write-then-read must `flush()` first. Never paper over this with `setTimeout` or tick
131+
counts. `@solidjs/testing-library`'s `render` populates the container synchronously; subsequent
132+
updates need `flush()`.
101133
- Return getter objects (not objects of accessors, not memoized fresh objects) from composables
102134
whose result is read once by a Solid context or passed as a prop.
103135

136+
### Store mirror
137+
138+
`createStore` and `reconcile` are exported from **`solid-js`** now, not `solid-js/store`.
139+
140+
- `reconcile(value, key?)``key` is the **2nd positional argument** and defaults to `'id'`,
141+
which is exactly what this port needs. The 1.x `{ key, merge }` options object is not the 2.0
142+
shape and throws.
143+
- `createProjection(fn, seed, options?)` is a derived, **read-only** store with the same `'id'`
144+
default key. It can be driven from a non-reactive external source (the manager's subscribe
145+
callback) by bumping a version signal from that callback and reading the signal in `fn` — proven
146+
at step 1.5.
147+
104148
### DOM parity
105149

106150
- Build class strings with core's `clsx` exclusively. Never template interpolation.
@@ -110,6 +154,9 @@ type-only module erased by the bundler), and allows `./foo.jsx` under `dist/sour
110154

111155
### Types
112156

157+
- **`jsxImportSource` is `"@solidjs/web"`.** `solid-js@2` owns no JSX namespace and no
158+
`jsx-runtime`. `JSX` and `ComponentProps` import from `@solidjs/web`; `Component` stays on
159+
`solid-js`.
113160
- `ReactNode``LabelNode` (`JSX.Element | string`); titles stay `string`.
114161
- `ComponentType<P>` → Solid's `Component<P>`.
115162
- Use `import type` for type-only imports (`verbatimModuleSyntax` is on).
@@ -128,15 +175,22 @@ type-only module erased by the bundler), and allows `./foo.jsx` under `dist/sour
128175
**Standing rule: every gate must be proven to fail.** When a step adds a gate, deliberately break
129176
it, record that it went red, then revert. A gate that cannot fail is worse than none.
130177

131-
Current gates (step 1): `fmt:check`, `build`, `check`, `check:exports`, `lint`, `test:coverage`
132-
(global 80% lines — vacuous until step 3 adds real executable code in `src/reactive/`), `test:ssr`.
133-
(`conformance` is a stub that exits 0 until step 6; it is not a gate yet.)
178+
Current gates (step 1.5): `check:versions`, `fmt:check`, `build`, `check`, `check:exports`,
179+
`lint`, `test:coverage` (global 80% lines — vacuous until step 3 adds real executable code in
180+
`src/reactive/`), `test:ssr`. (`conformance` is a stub that exits 0 until step 6; it is not a
181+
gate yet.)
134182

135183
All five were proven red at step 1 and reverted: coverage (threshold to 99 + an injected
136184
uncovered function), export-condition **order** (`import` moved first), export-condition
137185
**target** (`solid` repointed at `dist/index.js`), the SSR **markup** assertion (component's label
138186
dropped), and `check-dist-specifiers` (a directory import appended to `dist/index.d.ts`).
139187

188+
**Four of the five were re-proved red on the Solid 2 toolchain at step 1.5** — a gate proved red
189+
under Solid 1 is not evidence about Solid 2, since the plugin, the resolver behavior and the SSR
190+
renderer all changed. Coverage, condition **order** (both layers fired), condition **target**, and
191+
SSR **markup** under the new synchronous `renderToString`. `check-dist-specifiers` is unaffected by
192+
the runtime swap; it was re-run against the rebuilt `dist` instead.
193+
140194
⚠️ Two assertion shapes that look like gates but are not, both found and removed in review — do
141195
not reintroduce them:
142196

@@ -163,5 +217,5 @@ is how CI runs it.
163217

164218
## Repo status
165219

166-
**Local only.** No `git init`, no GitHub remote. `.gitignore` and `.github/workflows/ci.yml` exist
167-
so they are in place whenever the repo is initialized.
220+
**Remote-less.** The repo is a local git repo with no GitHub remote and nothing pushed.
221+
`.github/workflows/ci.yml` exists so it is in place whenever a remote is added.

0 commit comments

Comments
 (0)