From 453b67121df7848cf2cbfe93698860628df71f26 Mon Sep 17 00:00:00 2001 From: Alexandru Mariuti Date: Wed, 13 May 2026 20:26:43 +0200 Subject: [PATCH 1/9] Rebuild Solid skill from scratch with pubspec-based detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill is rewritten to lead with pubspec.yaml detection of solid_annotations / solid_generator as the authoritative signal, so it applies even when the user never says "Solid". Two new capabilities: - Decision shortcuts table for the most common Flutter→Solid moves (adding widgets, fetch-on-change, lib/-edit diagnoses, etc.). - references/third-party-packages.md catalogue for redirecting go_router / freezed / riverpod / drift / json_serializable file creation from lib/ to source/ — the case where another AI follows package READMEs literally. scripts/verify.sh now chains dart fix --apply after build_runner so generated lib/ output is lint-clean (adds const, removes unused imports, prefers relative imports). dart fix failure is non-fatal. Adds evals/ with four seed prompts covering counter creation, reactive query, lib-edit migration, and go_router redirect — plus trigger_queries.json (20 should/should-not queries) for running the skill-creator description optimization loop later. Docs: new "Clean up after generation" subsection in guides/getting-started.mdx with dev + CI guidance for dart fix. --- .../content/docs/guides/getting-started.mdx | 19 +++ skills/solid/SKILL.md | 148 ++++++++++++++---- skills/solid/evals/evals.json | 81 ++++++++++ .../evals/files/eval-1/analysis_options.yaml | 9 ++ skills/solid/evals/files/eval-1/build.yaml | 6 + skills/solid/evals/files/eval-1/pubspec.yaml | 23 +++ .../solid/evals/files/eval-1/source/main.dart | 22 +++ .../evals/files/eval-2/analysis_options.yaml | 9 ++ skills/solid/evals/files/eval-2/build.yaml | 6 + skills/solid/evals/files/eval-2/pubspec.yaml | 23 +++ .../solid/evals/files/eval-2/source/main.dart | 22 +++ .../evals/files/eval-3/analysis_options.yaml | 9 ++ skills/solid/evals/files/eval-3/build.yaml | 6 + .../solid/evals/files/eval-3/lib/counter.dart | 39 +++++ skills/solid/evals/files/eval-3/pubspec.yaml | 23 +++ .../evals/files/eval-3/source/counter.dart | 21 +++ .../solid/evals/files/eval-3/source/main.dart | 22 +++ .../evals/files/eval-4/analysis_options.yaml | 9 ++ skills/solid/evals/files/eval-4/build.yaml | 6 + skills/solid/evals/files/eval-4/pubspec.yaml | 23 +++ .../solid/evals/files/eval-4/source/main.dart | 22 +++ skills/solid/evals/trigger_queries.json | 82 ++++++++++ .../solid/references/annotation-contract.md | 22 +-- skills/solid/references/patterns.md | 8 +- .../solid/references/third-party-packages.md | 116 ++++++++++++++ skills/solid/references/troubleshooting.md | 5 +- skills/solid/scripts/scaffold-widget.sh | 4 +- skills/solid/scripts/verify.sh | 45 ++++-- 28 files changed, 764 insertions(+), 66 deletions(-) create mode 100644 skills/solid/evals/evals.json create mode 100644 skills/solid/evals/files/eval-1/analysis_options.yaml create mode 100644 skills/solid/evals/files/eval-1/build.yaml create mode 100644 skills/solid/evals/files/eval-1/pubspec.yaml create mode 100644 skills/solid/evals/files/eval-1/source/main.dart create mode 100644 skills/solid/evals/files/eval-2/analysis_options.yaml create mode 100644 skills/solid/evals/files/eval-2/build.yaml create mode 100644 skills/solid/evals/files/eval-2/pubspec.yaml create mode 100644 skills/solid/evals/files/eval-2/source/main.dart create mode 100644 skills/solid/evals/files/eval-3/analysis_options.yaml create mode 100644 skills/solid/evals/files/eval-3/build.yaml create mode 100644 skills/solid/evals/files/eval-3/lib/counter.dart create mode 100644 skills/solid/evals/files/eval-3/pubspec.yaml create mode 100644 skills/solid/evals/files/eval-3/source/counter.dart create mode 100644 skills/solid/evals/files/eval-3/source/main.dart create mode 100644 skills/solid/evals/files/eval-4/analysis_options.yaml create mode 100644 skills/solid/evals/files/eval-4/build.yaml create mode 100644 skills/solid/evals/files/eval-4/pubspec.yaml create mode 100644 skills/solid/evals/files/eval-4/source/main.dart create mode 100644 skills/solid/evals/trigger_queries.json create mode 100644 skills/solid/references/third-party-packages.md diff --git a/docs/src/content/docs/guides/getting-started.mdx b/docs/src/content/docs/guides/getting-started.mdx index 10a3fad..4103755 100644 --- a/docs/src/content/docs/guides/getting-started.mdx +++ b/docs/src/content/docs/guides/getting-started.mdx @@ -136,6 +136,25 @@ dart run build_runner watch --delete-conflicting-outputs - Press `r` in the `flutter run` terminal after `build_runner` emits. - Use [`dashmonx`](https://pub.dev/packages/dashmonx), which wraps `flutter run` and triggers hot reload automatically when files under `lib/` change. Any `flutter run` flag passes through, e.g. `dashmonx -d chrome` for a web target. +### Clean up after generation + +Solid prioritises producing correct, runnable code over polishing it. The generated `lib/` output may miss some `const` opportunities, leave unused imports, or pick a non-preferred import form. Run `dart fix` after `build_runner` to apply the lint-driven fixes (`prefer_const_constructors`, `unnecessary_import`, `prefer_relative_imports`, …): + +```bash +dart fix --apply +``` + +In CI, chain the two commands so the generated output is always lint-clean: + +```bash +dart run build_runner build --delete-conflicting-outputs +dart fix --apply +``` + + + diff --git a/skills/solid/SKILL.md b/skills/solid/SKILL.md index aba602f..47cb556 100644 --- a/skills/solid/SKILL.md +++ b/skills/solid/SKILL.md @@ -1,59 +1,125 @@ --- name: solid -description: Use when writing Flutter code that uses the Solid framework — annotations like @SolidState, @SolidEffect, @SolidQuery, @SolidEnvironment in source/ files. Solid is a codegen layer where source/ is the source of truth and lib/ is generated. +description: > + Use this skill whenever working on a Dart/Flutter project whose `pubspec.yaml` + declares `solid_annotations` or `solid_generator` — that is the authoritative + signal, even when the user never says "Solid". In Solid projects, the + `source/` directory is the source of truth and `lib/` is generated by + build_runner; every code edit goes under `source/`, never `lib/`. Trigger on: + adding any widget, page, controller, route, or model; reactive patterns + ("when X changes fetch Y", "debounce this query", "make this reactive"); + installing pub packages like go_router, freezed, riverpod, drift, + json_serializable (their docs say `lib/` — substitute `source/`); diagnosing + "my lib/ edits keep disappearing"; or any `@SolidState`, `@SolidEffect`, + `@SolidQuery`, `@SolidEnvironment`, or `.untracked` usage. Do NOT trigger for + SolidJS / Solid.js (unrelated JavaScript framework) or Flutter projects + without `solid_annotations`/`solid_generator` in pubspec.yaml. --- # Solid (Flutter) -Solid is a tiny framework on top of Flutter. You write reactive state directly on `StatelessWidget` in `source/`; the `solid_generator` `build_runner` builder transpiles each `source/.dart` to `lib/.dart`. Inspired by SwiftUI (`@Environment`) and SolidJS (fine-grained reactivity). Backed by [`flutter_solidart`](https://pub.dev/packages/flutter_solidart). +Solid is a tiny framework on top of Flutter. You write reactive state directly on a `StatelessWidget` in `source/`; the `solid_generator` build_runner builder transpiles each `source/.dart` into `lib/.dart`, turning your class into a `StatefulWidget` with `Signal`/`Computed`/`Effect`/`Resource` plumbing from [`flutter_solidart`](https://pub.dev/packages/flutter_solidart). Inspired by SwiftUI (`@Environment`) and SolidJS (fine-grained reactivity). + +User-facing docs: . + +## Step 0 — Is this project Solid? + +Before doing anything, decide if the project actually uses Solid. The user may not say "Solid" anywhere — and an existing `lib/` directory plus `solid_annotations`/`solid_generator` not being in pubspec means this is *not* a Solid project. + +Run this check first: + +```bash +grep -E '^\s*(solid_annotations|solid_generator):' pubspec.yaml +``` + +- **Match found** → this is a Solid project. Follow this skill. +- **No match** → not a Solid project. Don't apply Solid conventions. Skip this skill. + +The same check via Read tool works too: read `pubspec.yaml` and look for either package name under `dependencies:` or `dev_dependencies:`. A Solid setup typically has `solid_annotations` (runtime) under `dependencies` and `solid_generator` plus `build_runner` under `dev_dependencies`. ## Cardinal rule -**Always edit `source/`. Never edit `lib/` by hand.** `lib/` is regenerated by `dart run build_runner build` / `watch` and any manual edits are lost. If you need to add a widget, create the file in `source/`, then run `build_runner`. The user-facing app entry point is `source/main.dart`; `lib/main.dart` is generated. +**Edit `source/.dart`. Never edit `lib/.dart`.** + +In Flutter, `lib/` is where you write code. In Solid, `lib/` is generated output — every time build_runner runs, it overwrites `lib/.dart` from `source/.dart`. Hand-edits to `lib/` are lost on the next build. The user-facing entry point is `source/main.dart`; `lib/main.dart` is generated. + +This inverts Flutter muscle memory. The rest of the Flutter ecosystem (`flutter run`, pub packages, IDE templates, every tutorial on the internet) assumes `lib/` is the source of truth. In this project it isn't. Apply the substitution everywhere. + +## Decision shortcuts + +| Situation | What to do | +| --- | --- | +| About to write to a file under `lib/` | **Stop.** Find or create the matching `source/.dart` and write there. | +| User asks to "add a widget / page / button / form" | Create `source/.dart`. Use `@SolidState` for fields you'll mutate. | +| User asks to "fetch X when Y changes" | `@SolidState` for the input Y, `@SolidQuery` (no parameters) for the fetch. Body reads Y to register the dependency. | +| User asks to install a pub package whose README says "create `lib/.dart`" | Substitute `source/` for `lib/` in every file-creation step. See `references/third-party-packages.md`. | +| User says "I edited `lib/foo.dart` and the change disappeared" | The `lib/` write is the bug, not build_runner. Migrate the change to `source/foo.dart`, then regenerate. | +| build_runner output looks unpolished (no `const`, unused imports) | Run `scripts/verify.sh`, which chains `dart fix --apply` after build_runner. | +| build_runner fails | Run `scripts/verify.sh` from the package root — it surfaces the first `[SEVERE]` line. | + +## Third-party packages: substitute `source/` for `lib/` + +When the user (or another AI) installs a new pub package, the package's README, examples, and any AI-generated setup instructions will all assume `lib/` is the source of truth. In a Solid project that assumption is wrong. -**Same-package imports must be relative.** Inside a `source/` file, reference other source files via relative paths (`../controllers/foo.dart`), never via `package:/foo.dart`. The `package:` form resolves to `lib/` (the generated realm), pointing a source file at the lowered Signal types — and the generator now rejects it at build time. Cross-package imports (`flutter`, `solid_annotations`, `provider`, third-party) keep the `package:` form as usual. +**Rule of thumb**: the package itself stays in `pubspec.yaml` as the docs describe. Only the *files you write that import it* move from `lib/` to `source/`. Examples: + +- `go_router` README says "create `lib/router.dart`" → create `source/router.dart` instead, and import it from `source/main.dart` via a relative path. +- `freezed` says "create `lib/models/user.dart`" → create `source/models/user.dart`. Freezed's own `*.freezed.dart` generated output still lands wherever `build.yaml` puts it (typically next to the source file under `lib/`, since freezed reads from `lib/`); **but** in a Solid project you want freezed to read from `source/` too. Add `source/**` to freezed's `build.yaml` `sources` list (Solid's setup already does this — keep it). +- `riverpod` generator says "create `lib/providers/...`" → create `source/providers/...`. +- `drift` says "create `lib/database.dart`" → create `source/database.dart`. + +For the full list and per-package gotchas, read `references/third-party-packages.md`. + +**The key thing to tell yourself**: "the docs say `lib/`. In this project, that means `source/`." + +## Same-package imports must be relative + +Inside a `source/` file, reference other source files via relative paths (`../controllers/foo.dart`), never via `package:/foo.dart`. The `package:` form resolves to `lib/` (the *generated* realm), pointing your source file at lowered Signal types — the generator now rejects it at build time. Cross-package imports (`flutter`, `solid_annotations`, `provider`, third-party) keep the `package:` form as usual. ## Annotation cheat sheet Each annotation goes on a class member of a `StatelessWidget` (or any class — `@SolidEnvironment` also works in `State`). The generator turns the widget into a `StatefulWidget` under the hood. - **`@SolidState()`** — reactive state. Docs: . - - Valid on: instance field with initializer, `late` non-nullable instance field, instance getter (derived state). + - Valid on: instance field with initializer, `late` non-nullable instance field, nullable instance field, instance getter (derived state). - Invalid: `final`, `const`, `static`, setter, method, top-level. - - Example: `@SolidState() int counter = 0;` or `@SolidState() int get doubleCounter => counter * 2;` + - Example: `@SolidState() int counter = 0;` or `@SolidState() int get doubleCounter => counter * 2;`. - **`@SolidEffect()`** — side effect that re-runs whenever its tracked dependencies change. Docs: . - Valid on: instance method returning `void`. - - Example: `@SolidEffect() void logCounter() { print('Counter: $counter'); }` + - Example: `@SolidEffect() void logCounter() { print('Counter: $counter'); }`. - **`@SolidQuery()`** — reactive async/stream resource. Docs: . - Valid on: instance method returning `Future` or `Stream`. **No parameters.** - Call site `fetchData()` returns a `Resource` exposing `.when(ready:, loading:, error:)`, `.maybeWhen(...)`, `.isRefreshing`, `.refresh()`. - Options: `debounce: Duration(...)`, `useRefreshing: false`. - - Example: `@SolidQuery() Future fetchData() async { ... }` then `fetchData().when(ready: Text.new, loading: CircularProgressIndicator.new, error: (e, _) => Text('$e'))`. + - Read `@SolidState` fields from the body to make the query react to them. -- **`@SolidEnvironment()`** — inject a value from the widget tree (SwiftUI `@Environment`-style). Docs: . +- **`@SolidEnvironment()`** — inject a value from the widget tree (SwiftUI-style). Docs: . - Valid on: `late` field on a `StatelessWidget` or `State`. - - Bound on first access to the nearest ancestor `Provider`. Reactive — `@SolidState` fields on the injected type stay reactive. - - Provide via `.environment()` extension or `Provider` from `package:provider`. - - Example: `@SolidEnvironment() late Counter counter;` then `child: CounterDisplay().environment((_) => Counter())`. + - Bound on first access to the nearest ancestor `Provider`. + - Provide via `.environment()` extension shipped by `solid_annotations`, or `Provider` from `package:provider`. + +For full target rules per annotation, read `references/annotation-contract.md`. For canonical idioms, read `references/patterns.md`. ## Untracked reads -By default every read of a `@SolidState` field inside `build`, `@SolidEffect`, or `@SolidQuery` registers a dependency on the surrounding computation — `build` re-renders the read site, effects re-fire, queries re-execute. Two ways to read without registering a dependency: +By default every read of a `@SolidState` field inside `build`, `@SolidEffect`, or `@SolidQuery` registers a dependency. Two opt-outs: + +- **Automatic**: reads inside callback parameters whose name starts with `on` (`onPressed`, `onTap`, `onChanged`, …) are untracked — Solid recognizes user-interaction handlers and doesn't subscribe. +- **Manual**: append `.untracked` to the field. Common use: `key: ValueKey(counter.untracked)`, or reading the same signal you're writing to inside an effect to avoid a self-dependency loop. -- **Automatic**: reads inside callback parameters whose name starts with `on` (`onPressed`, `onTap`, `onChanged`, …) are untracked — Solid treats them as gesture handlers. You don't write anything special. -- **Manual**: append `.untracked` to the field for a one-off untracked read. Common case is `key: ValueKey(counter.untracked)` (build the key once, don't rebuild on later changes), or reading the same signal you're writing to inside an effect to avoid a self-dependency loop: `history = [...history.untracked, counter];`. +In string interpolations, only the long form works: `'${counter.untracked}'`. The short form `'$counter.untracked'` parses as `${counter}` followed by a literal suffix (still tracked). -In string interpolations use the long form `'${counter.untracked}'` — the short form `'$counter.untracked'` parses as `${counter}` followed by a literal suffix (still tracked). Docs: . +Docs: . -For full target rules (every valid/invalid form with rationale) see `references/annotation-contract.md`. For canonical idioms see `references/patterns.md`. For error messages and fixes see `references/troubleshooting.md`. +## Setup checklist (fresh project) -## Setup checklist +If `pubspec.yaml` doesn't yet declare Solid, install it: 1. `flutter pub add solid_annotations flutter_solidart provider` 2. `dart pub add --dev solid_generator build_runner` -3. Add `source/**` to `build.yaml`: +3. Create `build.yaml` at the project root (or extend the existing one): ```yaml targets: $default: @@ -62,32 +128,46 @@ For full target rules (every valid/invalid form with rationale) see `references/ - lib/** - $package$ ``` -4. In `source/main.dart`, set `SolidartConfig.autoDispose = false` before `runApp(...)`. (Temporary — will become the default in a future `flutter_solidart` major release.) -5. Run `dart run build_runner watch --delete-conflicting-outputs` during development. +4. In `source/main.dart`, set `SolidartConfig.autoDispose = false;` before `runApp(...)`. (Temporary — will become the default in a future `flutter_solidart` major release.) +5. In `analysis_options.yaml`, add `analyzer.errors.must_be_immutable: ignore` (your source widgets are mutable; the generated ones are immutable). +6. Run `dart run build_runner watch --delete-conflicting-outputs` during development. + +## Verify your changes + +After writing or editing `source/`, regenerate `lib/` and apply lint fixes: + +- **`scripts/verify.sh`** — run from any package root. Runs `dart run build_runner build --delete-conflicting-outputs`, then `dart fix --apply` on the package (adds `const`, removes unused imports, applies relative-import lints). Prints PASS/FAIL plus the first `[SEVERE]` error on failure. Exit code reflects build_runner success; `dart fix` failure is non-fatal. + +Why `dart fix --apply` matters: the generator prioritises correct, runnable code over polish. The emitted `lib/` may miss `const` opportunities, leave unused imports, or pick a non-preferred import form. `dart fix --apply` cleans this up using the project's lint rules (`prefer_const_constructors`, `unnecessary_import`, `prefer_relative_imports`, …). Always run it after generation — in CI too. ## Hot reload -`flutter run` does not auto-reload when `build_runner` rewrites `lib/`. Two workflows: -- Press `r` in the `flutter run` terminal after `build_runner` emits. -- Use [`dashmonx`](https://pub.dev/packages/dashmonx) — it wraps `flutter run` and triggers reload on `lib/` changes. +`flutter run` does not auto-reload when build_runner rewrites `lib/` (no IDE save event fires for filesystem changes). Two workflows: + +- Press `r` in the `flutter run` terminal after build_runner emits. +- Use [`dashmonx`](https://pub.dev/packages/dashmonx) — wraps `flutter run` and triggers hot reload on `lib/` changes. Any `flutter run` flag passes through, e.g. `dashmonx -d chrome`. ## Common mistakes -- Editing `lib/` files. They are regenerated; your edits are lost. +- Writing to a file under `lib/`. Always under `source/`. (The single hardest rule to internalise.) +- Following a pub-package README literally when it says `lib/`. Substitute `source/`. See `references/third-party-packages.md`. - Adding `final` or `static` to a `@SolidState` field. The generator rejects it. -- Forgetting `SolidartConfig.autoDispose = false` — atoms are not disposed and tests / long sessions leak. -- Giving `@SolidQuery` parameters. Use `@SolidState` fields as inputs instead — the query re-runs when they change. -- Expecting `flutter run` to pick up `build_runner` output without `r` or `dashmonx`. -- Treating `must_be_immutable` lint as a real error. The widgets you write are mutable; the generated ones are immutable. Set `must_be_immutable: ignore` in `analysis_options.yaml`. +- Giving `@SolidQuery` parameters. Use `@SolidState` fields as inputs — the query re-runs when they change. +- Forgetting `SolidartConfig.autoDispose = false` in `source/main.dart` — atoms leak in tests and long sessions. +- Importing same-package files via `package:/...` from inside `source/`. Use relative paths. +- Expecting `flutter run` to pick up build_runner output without `r` or `dashmonx`. +- Treating `must_be_immutable` lint as a real error — your widgets are mutable; the generated ones are immutable. Set `must_be_immutable: ignore`. ## Helper scripts -The skill ships two scripts under `scripts/`: - -- **`scripts/verify.sh`** — run from any package root. Runs `dart run build_runner build --delete-conflicting-outputs`, prints PASS/FAIL plus the first error. Exit code reflects success. -- **`scripts/scaffold-widget.sh [--state|--query|--env]`** — writes a starter `source/.dart` with the right boilerplate for the chosen annotation. Refuses to overwrite existing files. +- **`scripts/verify.sh`** — described above. +- **`scripts/scaffold-widget.sh [--state|--query|--env]`** — writes a starter `source/.dart` with the right boilerplate. Refuses to overwrite. ## Where to read more - Canonical docs: -- Working example on GitHub: +- Annotation valid/invalid targets: `references/annotation-contract.md` +- Canonical idioms (counter, computed, effect, query, environment, untracked): `references/patterns.md` +- Error symptom → cause → fix: `references/troubleshooting.md` +- Third-party package redirect catalogue: `references/third-party-packages.md` +- Working example: diff --git a/skills/solid/evals/evals.json b/skills/solid/evals/evals.json new file mode 100644 index 0000000..cbbe2b3 --- /dev/null +++ b/skills/solid/evals/evals.json @@ -0,0 +1,81 @@ +{ + "skill_name": "solid", + "evals": [ + { + "id": 1, + "prompt": "Add a counter widget to my Flutter app — tap + and the number bumps. Make it a fresh page.", + "expected_output": "A new widget file created under source/counter.dart (or source/counter_page.dart) using @SolidState for the counter field. No lib/ files created or edited by the agent. The user is told to run build_runner (or scripts/verify.sh) afterward.", + "files": [ + "evals/files/eval-1/pubspec.yaml", + "evals/files/eval-1/build.yaml", + "evals/files/eval-1/analysis_options.yaml", + "evals/files/eval-1/source/main.dart" + ], + "expectations": [ + "The agent created a new file under source/ (path matches source/*.dart) — not under lib/.", + "The new file uses @SolidState on at least one mutable field (e.g. `@SolidState() int counter = 0;`).", + "The new file imports 'package:solid_annotations/solid_annotations.dart'.", + "The new file does NOT define an empty `void dispose() {}` override on the StatelessWidget (the generator synthesizes dispose for reactive fields).", + "No file under lib/ was created or modified by the agent.", + "The agent recommends running `dart run build_runner build` (or `scripts/verify.sh`, or `build_runner watch`) so the new widget is transpiled into lib/." + ] + }, + { + "id": 2, + "prompt": "Fetch posts whenever the selected userId changes. Debounce 500ms.", + "expected_output": "A widget (or modification to an existing widget) that has a @SolidState field for the userId and a @SolidQuery method (no parameters) that fetches based on that field, with debounce: Duration(milliseconds: 500). Rendered with .when(ready:, loading:, error:). All edits under source/, none under lib/.", + "files": [ + "evals/files/eval-2/pubspec.yaml", + "evals/files/eval-2/build.yaml", + "evals/files/eval-2/analysis_options.yaml", + "evals/files/eval-2/source/main.dart" + ], + "expectations": [ + "The new or modified widget is under source/, not lib/.", + "The widget declares a @SolidState field (typically String? userId or similar) that the query reads from its body.", + "The query method is annotated @SolidQuery(...) with debounce: Duration(milliseconds: 500).", + "The @SolidQuery method takes no parameters (the body reads @SolidState fields directly).", + "The query is rendered with a `.when(ready:, loading:, error:)` block (or `.maybeWhen`).", + "No lib/ file was created or modified by the agent." + ] + }, + { + "id": 3, + "prompt": "I keep editing lib/counter.dart to change the AppBar title from 'Counter' to 'My Counter' but the change keeps disappearing on save. What's going on, and how do I fix it?", + "expected_output": "The agent diagnoses that lib/counter.dart is generated from source/counter.dart and explains why edits to lib/ are overwritten. The agent then makes the change in source/counter.dart (changing the AppBar title string from 'Counter' to 'My Counter') and tells the user to re-run build_runner. The agent must NOT edit lib/counter.dart.", + "files": [ + "evals/files/eval-3/pubspec.yaml", + "evals/files/eval-3/build.yaml", + "evals/files/eval-3/analysis_options.yaml", + "evals/files/eval-3/source/main.dart", + "evals/files/eval-3/source/counter.dart", + "evals/files/eval-3/lib/counter.dart" + ], + "expectations": [ + "The agent explains that lib/ is generated from source/ and that build_runner overwrites lib/ edits — using language the user can act on, not jargon.", + "The agent edits source/counter.dart to change the AppBar title from 'Counter' to 'My Counter'.", + "The agent does NOT modify or write to lib/counter.dart.", + "The agent recommends re-running `dart run build_runner build` (or `scripts/verify.sh`) to regenerate lib/counter.dart from the updated source." + ] + }, + { + "id": 4, + "prompt": "Add go_router and set up two routes: `/` → HomePage, `/settings` → SettingsPage. Keep my existing app structure.", + "expected_output": "go_router added to pubspec.yaml. A new source/router.dart created with the GoRouter configuration. source/main.dart edited to use MaterialApp.router with the new router config. source/home_page.dart and source/settings_page.dart created (or stub equivalents). NO lib/ files created or edited by the agent — even though the go_router README would normally direct the agent to create lib/router.dart.", + "files": [ + "evals/files/eval-4/pubspec.yaml", + "evals/files/eval-4/build.yaml", + "evals/files/eval-4/analysis_options.yaml", + "evals/files/eval-4/source/main.dart" + ], + "expectations": [ + "go_router was added as a dependency in pubspec.yaml.", + "A new file `source/router.dart` (or similar under source/) exists and contains a `GoRouter` configuration declaring the `/` and `/settings` routes.", + "source/main.dart was edited to use `MaterialApp.router` (or equivalent) wired to the GoRouter config.", + "Any imports between source/ files use relative paths (e.g. `import 'router.dart';`), NOT `package:/router.dart`.", + "No file under lib/ was created or modified by the agent — even though go_router's README normally instructs creating lib/router.dart.", + "The agent recommends running build_runner (or `scripts/verify.sh`) after the source/ edits so lib/ is regenerated." + ] + } + ] +} diff --git a/skills/solid/evals/files/eval-1/analysis_options.yaml b/skills/solid/evals/files/eval-1/analysis_options.yaml new file mode 100644 index 0000000..1659294 --- /dev/null +++ b/skills/solid/evals/files/eval-1/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + errors: + must_be_immutable: ignore +linter: + rules: + public_member_api_docs: false + always_use_package_imports: false + prefer_relative_imports: true diff --git a/skills/solid/evals/files/eval-1/build.yaml b/skills/solid/evals/files/eval-1/build.yaml new file mode 100644 index 0000000..673bd1d --- /dev/null +++ b/skills/solid/evals/files/eval-1/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - source/** + - lib/** + - $package$ diff --git a/skills/solid/evals/files/eval-1/pubspec.yaml b/skills/solid/evals/files/eval-1/pubspec.yaml new file mode 100644 index 0000000..ab77fbb --- /dev/null +++ b/skills/solid/evals/files/eval-1/pubspec.yaml @@ -0,0 +1,23 @@ +name: my_app +description: Sample Flutter app using Solid. +publish_to: none +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + flutter_solidart: ^2.0.0 + solid_annotations: ^2.0.0 + provider: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.0 + solid_generator: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/skills/solid/evals/files/eval-1/source/main.dart b/skills/solid/evals/files/eval-1/source/main.dart new file mode 100644 index 0000000..b593477 --- /dev/null +++ b/skills/solid/evals/files/eval-1/source/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + home: Scaffold( + appBar: AppBar(title: const Text('My App')), + body: const Center(child: Text('Hello, world!')), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-2/analysis_options.yaml b/skills/solid/evals/files/eval-2/analysis_options.yaml new file mode 100644 index 0000000..1659294 --- /dev/null +++ b/skills/solid/evals/files/eval-2/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + errors: + must_be_immutable: ignore +linter: + rules: + public_member_api_docs: false + always_use_package_imports: false + prefer_relative_imports: true diff --git a/skills/solid/evals/files/eval-2/build.yaml b/skills/solid/evals/files/eval-2/build.yaml new file mode 100644 index 0000000..673bd1d --- /dev/null +++ b/skills/solid/evals/files/eval-2/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - source/** + - lib/** + - $package$ diff --git a/skills/solid/evals/files/eval-2/pubspec.yaml b/skills/solid/evals/files/eval-2/pubspec.yaml new file mode 100644 index 0000000..ab77fbb --- /dev/null +++ b/skills/solid/evals/files/eval-2/pubspec.yaml @@ -0,0 +1,23 @@ +name: my_app +description: Sample Flutter app using Solid. +publish_to: none +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + flutter_solidart: ^2.0.0 + solid_annotations: ^2.0.0 + provider: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.0 + solid_generator: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/skills/solid/evals/files/eval-2/source/main.dart b/skills/solid/evals/files/eval-2/source/main.dart new file mode 100644 index 0000000..b593477 --- /dev/null +++ b/skills/solid/evals/files/eval-2/source/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + home: Scaffold( + appBar: AppBar(title: const Text('My App')), + body: const Center(child: Text('Hello, world!')), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-3/analysis_options.yaml b/skills/solid/evals/files/eval-3/analysis_options.yaml new file mode 100644 index 0000000..1659294 --- /dev/null +++ b/skills/solid/evals/files/eval-3/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + errors: + must_be_immutable: ignore +linter: + rules: + public_member_api_docs: false + always_use_package_imports: false + prefer_relative_imports: true diff --git a/skills/solid/evals/files/eval-3/build.yaml b/skills/solid/evals/files/eval-3/build.yaml new file mode 100644 index 0000000..673bd1d --- /dev/null +++ b/skills/solid/evals/files/eval-3/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - source/** + - lib/** + - $package$ diff --git a/skills/solid/evals/files/eval-3/lib/counter.dart b/skills/solid/evals/files/eval-3/lib/counter.dart new file mode 100644 index 0000000..c433ca0 --- /dev/null +++ b/skills/solid/evals/files/eval-3/lib/counter.dart @@ -0,0 +1,39 @@ +// GENERATED FILE — DO NOT EDIT BY HAND. +// Source: source/counter.dart +// The user attempted to change the AppBar title from 'Counter' to 'My Counter' +// directly in this file. build_runner's next run will overwrite their change. +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +class CounterPage extends StatefulWidget { + const CounterPage({super.key}); + + @override + State createState() => _CounterPageState(); +} + +class _CounterPageState extends State { + final counter = Signal(0, name: 'counter'); + + @override + void dispose() { + counter.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('My Counter')), // user's mis-edit, will be overwritten + body: Center( + child: SignalBuilder( + builder: (context, _) => Text('Counter is ${counter.value}'), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => counter.value++, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-3/pubspec.yaml b/skills/solid/evals/files/eval-3/pubspec.yaml new file mode 100644 index 0000000..ab77fbb --- /dev/null +++ b/skills/solid/evals/files/eval-3/pubspec.yaml @@ -0,0 +1,23 @@ +name: my_app +description: Sample Flutter app using Solid. +publish_to: none +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + flutter_solidart: ^2.0.0 + solid_annotations: ^2.0.0 + provider: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.0 + solid_generator: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/skills/solid/evals/files/eval-3/source/counter.dart b/skills/solid/evals/files/eval-3/source/counter.dart new file mode 100644 index 0000000..55f4462 --- /dev/null +++ b/skills/solid/evals/files/eval-3/source/counter.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:solid_annotations/solid_annotations.dart'; + +class CounterPage extends StatelessWidget { + CounterPage({super.key}); + + @SolidState() + int counter = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Counter')), + body: Center(child: Text('Counter is $counter')), + floatingActionButton: FloatingActionButton( + onPressed: () => counter++, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-3/source/main.dart b/skills/solid/evals/files/eval-3/source/main.dart new file mode 100644 index 0000000..b593477 --- /dev/null +++ b/skills/solid/evals/files/eval-3/source/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + home: Scaffold( + appBar: AppBar(title: const Text('My App')), + body: const Center(child: Text('Hello, world!')), + ), + ); + } +} diff --git a/skills/solid/evals/files/eval-4/analysis_options.yaml b/skills/solid/evals/files/eval-4/analysis_options.yaml new file mode 100644 index 0000000..1659294 --- /dev/null +++ b/skills/solid/evals/files/eval-4/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + errors: + must_be_immutable: ignore +linter: + rules: + public_member_api_docs: false + always_use_package_imports: false + prefer_relative_imports: true diff --git a/skills/solid/evals/files/eval-4/build.yaml b/skills/solid/evals/files/eval-4/build.yaml new file mode 100644 index 0000000..673bd1d --- /dev/null +++ b/skills/solid/evals/files/eval-4/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - source/** + - lib/** + - $package$ diff --git a/skills/solid/evals/files/eval-4/pubspec.yaml b/skills/solid/evals/files/eval-4/pubspec.yaml new file mode 100644 index 0000000..ab77fbb --- /dev/null +++ b/skills/solid/evals/files/eval-4/pubspec.yaml @@ -0,0 +1,23 @@ +name: my_app +description: Sample Flutter app using Solid. +publish_to: none +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + flutter_solidart: ^2.0.0 + solid_annotations: ^2.0.0 + provider: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.0 + solid_generator: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/skills/solid/evals/files/eval-4/source/main.dart b/skills/solid/evals/files/eval-4/source/main.dart new file mode 100644 index 0000000..b593477 --- /dev/null +++ b/skills/solid/evals/files/eval-4/source/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + home: Scaffold( + appBar: AppBar(title: const Text('My App')), + body: const Center(child: Text('Hello, world!')), + ), + ); + } +} diff --git a/skills/solid/evals/trigger_queries.json b/skills/solid/evals/trigger_queries.json new file mode 100644 index 0000000..d2bfd1c --- /dev/null +++ b/skills/solid/evals/trigger_queries.json @@ -0,0 +1,82 @@ +[ + { + "query": "In my flutter app i need to add a counter widget — tap + and it counts up. I have solid_annotations in my pubspec.yaml. give me a fresh page for it", + "should_trigger": true + }, + { + "query": "ok so im trying to fetch user posts whenever the selected user id changes, with a 500ms debounce so we dont hammer the api. this is the flutter app where i'm already using @SolidQuery elsewhere", + "should_trigger": true + }, + { + "query": "weird bug — i keep editing lib/counter.dart to change a string and it keeps reverting after a few seconds. why?? my pubspec has build_runner and solid_generator listed", + "should_trigger": true + }, + { + "query": "i want to add go_router to this flutter app and set up 2 routes (/ and /settings). just looked at pubspec and we already use solid_annotations so i'm not sure where router.dart should go", + "should_trigger": true + }, + { + "query": "Add a SolidEffect that prints to console whenever the counter changes. The widget is in source/counter_page.dart and the counter is an @SolidState int field", + "should_trigger": true + }, + { + "query": "the build_runner is failing with 'requires assignable target' on my @SolidState field. heres the snippet: @SolidState() final int counter = 0;", + "should_trigger": true + }, + { + "query": "Make this widget reactive — when toggle is true the colour changes, when it's false the colour reverts. the file is at source/colour_picker.dart in a flutter project with solid_generator in dev_deps", + "should_trigger": true + }, + { + "query": "scaffold a new page that fetches `/api/profile` and shows a loading spinner while it resolves. flutter project, solid_annotations is in pubspec already", + "should_trigger": true + }, + { + "query": "I want to inject an AuthService into a bunch of widgets via @SolidEnvironment. how do I provide it from main()? this is a Flutter app already set up with the Solid build_runner", + "should_trigger": true + }, + { + "query": "Help me wire up freezed in this flutter project — i need a User model with email, name, age. pubspec already has solid_generator so i'm not sure if i write the model under source/ or lib/", + "should_trigger": true + }, + { + "query": "fix this Solid.js component — when the signal changes the JSX doesn't re-render properly. it's a vite + typescript project", + "should_trigger": false + }, + { + "query": "explain how Riverpod scoping works in flutter. when does a Provider get re-created? this is a pure riverpod app, no codegen", + "should_trigger": false + }, + { + "query": "im setting up GitHub Actions for my Flutter project. how do i make `flutter test` run on PRs? pubspec is a vanilla flutter app, no solid", + "should_trigger": false + }, + { + "query": "json_serializable is throwing 'no JsonKey for field email'. where do i put the @JsonKey annotation? this is a standard flutter project, no solid_annotations", + "should_trigger": false + }, + { + "query": "in vanilla flutter, when should I use ChangeNotifier vs ValueListenable for state management? we're not using any codegen libs", + "should_trigger": false + }, + { + "query": "what's the difference between StatefulWidget and StatelessWidget? im new to flutter and starting a fresh project, pubspec is empty besides flutter", + "should_trigger": false + }, + { + "query": "convert this React class component to React hooks. its a Next.js page using class component lifecycle methods", + "should_trigger": false + }, + { + "query": "explain how to use setState properly to avoid extra rebuilds in flutter. our codebase is standard flutter, no codegen libs in pubspec", + "should_trigger": false + }, + { + "query": "I keep getting 'Provider not found' in my flutter app — pubspec has `provider: ^6.0` and i wrapped in Provider. what am i missing? no other state libs", + "should_trigger": false + }, + { + "query": "configure my flutter project's launch.json in VS Code to start with --device web-server. flutter app, no solid packages installed", + "should_trigger": false + } +] diff --git a/skills/solid/references/annotation-contract.md b/skills/solid/references/annotation-contract.md index 469d22d..00eb002 100644 --- a/skills/solid/references/annotation-contract.md +++ b/skills/solid/references/annotation-contract.md @@ -1,6 +1,6 @@ # Solid annotation contract -Per-annotation valid/invalid targets. Distilled from the user docs at . +Per-annotation valid/invalid targets and rationale. Distilled from the user docs at . ## `@SolidState()` — reactive state @@ -23,7 +23,7 @@ Docs: . | `static` field | State is per-widget-instance, not per-class. | | Setter | A `@SolidState` write goes through the generated setter; you don't write your own. | | Method | Use `@SolidEffect` for side effects or `@SolidQuery` for async values. | -| Top-level / library variable | Annotation only applies to class members. | +| Top-level / library variable | The annotation only applies to class members. | ## `@SolidEffect()` — side effect @@ -43,7 +43,7 @@ The body's reads of `@SolidState` fields are tracked automatically. The effect r - Methods with parameters. - Static or top-level functions. -## `@SolidQuery()` — async/stream resource +## `@SolidQuery()` — async / stream resource Docs: . @@ -60,7 +60,7 @@ Docs: . - Return type must be `Future` or `Stream`. - The call site `fetchData()` does **not** return a `Future`/`Stream` — it returns a `Resource` exposing: - `.when(ready: ..., loading: ..., error: ...)` - - `.maybeWhen(...orElse: ...)` + - `.maybeWhen(..., orElse: ...)` - `.isRefreshing` (true while a re-execution is in flight) - `.refresh()` to manually re-run @@ -69,7 +69,7 @@ Docs: . | Option | Effect | | --- | --- | | `debounce: Duration(...)` | Wait this long after the last input change before re-running. | -| `useRefreshing` (default `true`) | On re-execution from a dependency change, the resource stays on the current value while refetching, and `.isRefreshing` becomes `true` (smoother UX, no loading flash). Pass `useRefreshing: false` to drop back into the `loading` state on each re-execution instead. | +| `useRefreshing` (default `true`) | On re-execution from a dependency change, the resource stays on the current value while refetching, and `.isRefreshing` becomes `true` (smoother UX, no loading flash). Pass `useRefreshing: false` to drop back into the `loading` state on each re-execution. | ## `@SolidEnvironment()` — inject from widget tree @@ -84,13 +84,13 @@ Docs: . ### Behavior - Lookup is lazy: the field initializer runs the first time the field is read. -- Resolves the nearest ancestor `Provider` where `T` is the field's declared type. +- Resolves the nearest ancestor `Provider` where `T` is the declared type. - Reading a `@SolidState` member of the injected instance stays reactive — fine-grained reactivity is preserved across the boundary. - Works inside `build`, `@SolidEffect`, `@SolidQuery` bodies, or any other context. ### Providing the instance -Two equivalent ways: +Two equivalent forms: ```dart // 1. .environment() extension shipped by solid_annotations @@ -100,7 +100,7 @@ home: CounterDisplay().environment((_) => Counter()), home: Provider(create: (_) => Counter(), child: CounterDisplay()), ``` -For multiple providers chain `.environment(...)` calls or use `MultiProvider` from `package:provider`. +For multiple providers, chain `.environment(...)` calls or use `MultiProvider` from `package:provider`. The type argument is inferred from the closure's return type. Pass it explicitly only when consumers should read by a supertype: `.environment((_) => RealAuthService())`. @@ -121,7 +121,7 @@ extension UntrackedExtension on T { } ``` -`counter.untracked` typechecks identically to `counter` and is a no-op at runtime. The generator detects the pattern at source level and rewrites it to the underlying `untrackedValue` primitive and excludes the read from the dependency set — `SignalBuilder` doesn't wrap it inside `build`, and an enclosing `@SolidEffect` / `@SolidQuery` won't re-fire on changes to it. +`counter.untracked` typechecks identically to `counter` and is a no-op at runtime. The generator detects the pattern at source level and rewrites it to the underlying `untrackedValue` primitive, excluding the read from the dependency set — `SignalBuilder` doesn't wrap it inside `build`, and an enclosing `@SolidEffect` / `@SolidQuery` won't re-fire on changes to it. ### Two ways reads become untracked @@ -132,9 +132,9 @@ extension UntrackedExtension on T { ### Hard rules -- **String interpolation form**: only `'${counter.untracked}'` works. The short form `'$counter.untracked'` parses as `${counter}` followed by a literal `.untracked` suffix and is still a tracked read. +- **String interpolation form**: only `'${counter.untracked}'` works. The short form `'$counter.untracked'` parses as `${counter}` followed by a literal `.untracked` suffix (still tracked). - **Shadowing**: a local variable that shadows the field disables the rewrite for that scope (the analyzer's identifier resolution wins). -- **No-op on non-reactive types**: applied to a non-`@SolidState` value the extension is identity at compile time *and* runtime — safe to leave in code that may or may not target a reactive field. +- **No-op on non-reactive types**: applied to a non-`@SolidState` value, the extension is identity at compile time and at runtime — safe to leave in code that may or may not target a reactive field. ### When to reach for it diff --git a/skills/solid/references/patterns.md b/skills/solid/references/patterns.md index 5c7eb7b..e45cdb3 100644 --- a/skills/solid/references/patterns.md +++ b/skills/solid/references/patterns.md @@ -1,6 +1,6 @@ # Solid patterns -Six canonical idioms, each with a complete `source/` snippet. All examples lifted from the user docs at . +Canonical idioms, each with a complete `source/` snippet. All examples lifted from the user docs at . ## 1. Counter with `@SolidState` field @@ -211,7 +211,7 @@ import 'package:provider/provider.dart'; home: Provider(create: (_) => Counter(), child: CounterDisplay()), ``` -For multiple providers chain `.environment(...)` calls or use `MultiProvider`: +For multiple providers, chain `.environment(...)` calls or use `MultiProvider`: ```dart HomePage() @@ -251,8 +251,8 @@ List history = []; @SolidEffect() void recordHistory() { - history = [...history.untracked, counter]; // counter is tracked, history is not + history = [...history.untracked, counter]; // counter tracked, history not } ``` -Reads inside `on*` callback parameters (`onPressed`, `onTap`, `onChanged`, …) are auto-untracked — no `.untracked` needed. In string interpolations only the long form `'${counter.untracked}'` works; `'$counter.untracked'` is still tracked. +Reads inside `on*` callback parameters (`onPressed`, `onTap`, `onChanged`, …) are auto-untracked — no `.untracked` needed. In string interpolations, only the long form `'${counter.untracked}'` works; `'$counter.untracked'` is still tracked. diff --git a/skills/solid/references/third-party-packages.md b/skills/solid/references/third-party-packages.md new file mode 100644 index 0000000..240548c --- /dev/null +++ b/skills/solid/references/third-party-packages.md @@ -0,0 +1,116 @@ +# Third-party packages in a Solid project + +Every pub package's README, every Stack Overflow answer, every AI-generated setup instruction assumes `lib/` is where you write code. In a Solid project that assumption is inverted: `source/` is where you write code, `lib/` is generated. + +This reference catalogues the redirect pattern for common packages. + +## The general rule + +When a package's docs say: + +- *"Create `lib/.dart` and put this in it..."* → create `source/.dart` instead. +- *"Register this in `lib/main.dart`..."* → edit `source/main.dart` instead. +- *"Import via `package:/.dart`..."* → from inside `source/`, use a **relative** import (`../path/to/x.dart`). The `package:/...` form resolves to `lib/` (the generated realm) and the generator rejects it. + +The package itself stays in `pubspec.yaml` exactly as the docs describe. Only the *Dart files you write that import it* move from `lib/` to `source/`. + +## When a new code generator is added + +If the new package is itself a `build_runner` builder (freezed, json_serializable, drift, riverpod_generator, isar_generator, mockito with `@GenerateMocks`, …), it reads from `lib/` by default. To make it read from `source/` too, ensure your `build.yaml` `targets.$default.sources` includes `source/**`: + +```yaml +targets: + $default: + sources: + - source/** + - lib/** + - $package$ +``` + +The Solid setup already puts `source/**` first — keep it there and other generators will pick up your source files automatically. + +## Per-package gotchas + +### `go_router` + +The README says: *"Create `lib/router.dart` with your `GoRouter` config and import it from `lib/main.dart`."* + +In a Solid project: +- Create `source/router.dart` with the `GoRouter` config. +- Import it from `source/main.dart` via a relative path: `import 'router.dart';`. +- No `lib/` files created or edited by you. build_runner produces `lib/router.dart` and `lib/main.dart` from your source. + +```dart title="source/router.dart" +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'home_page.dart'; +import 'settings_page.dart'; + +final appRouter = GoRouter( + routes: [ + GoRoute(path: '/', builder: (_, __) => const HomePage()), + GoRoute(path: '/settings', builder: (_, __) => const SettingsPage()), + ], +); +``` + +```dart title="source/main.dart" +import 'package:flutter/material.dart'; +import 'package:flutter_solidart/flutter_solidart.dart'; + +import 'router.dart'; + +void main() { + SolidartConfig.autoDispose = false; + runApp(MaterialApp.router(routerConfig: appRouter)); +} +``` + +### `freezed` + +The README tells you to create `lib/models/user.dart` and run `build_runner`. In a Solid project: +- Create `source/models/user.dart`. +- Freezed generates `*.freezed.dart` siblings. Where those land depends on `build.yaml`. If `source/**` is in your sources list (Solid's setup ensures this), freezed reads your `source/models/user.dart` and emits `source/models/user.freezed.dart`. Solid then transpiles both `source/` files into `lib/`. +- Inside `source/`, import the freezed-generated file via a relative `.freezed.dart` path: `part 'user.freezed.dart';`. + +### `json_serializable` + +Same redirect as freezed — create the annotated class under `source/`, let the generator produce the `.g.dart` sibling under `source/`, and Solid copies the whole thing to `lib/` on build. + +### `riverpod` / `flutter_riverpod` + `riverpod_generator` + +Riverpod docs say: *"Define providers in `lib/providers/`."* In Solid: +- Create `source/providers/.dart`. +- `riverpod_generator` emits `*.g.dart` siblings — same deal as freezed. +- Providers read in widgets via `ref.watch(...)` work without modification, because by the time the code runs, it's the generated `lib/` version. + +That said: if you're using Riverpod, you may not need Solid at all (both are reactive state libraries). Mixing them in one project is unusual but supported — Solid handles the widget-local state, Riverpod handles app-global. + +### `drift` + +Drift docs say create `lib/database.dart`. In Solid: `source/database.dart`. Drift's generated `*.g.dart` lands as a sibling. + +### `isar` + +Same — `source/.dart` for the annotated classes. + +### `get_it` + +`get_it` doesn't generate code; it's runtime DI. The README still tells you to call `GetIt.I.registerSingleton(...)` in `lib/main.dart`. Substitute `source/main.dart`. + +### `flutter_bloc` + +The pattern guides write `lib/blocs/_bloc.dart` and `lib/blocs/_event.dart` etc. Substitute `source/blocs/_bloc.dart` etc. + +### `provider` + +`provider` is already a Solid dependency (it backs `@SolidEnvironment`). Use it directly when you need multiple providers or when the `.environment(...)` chain gets long. + +### `dio` / `http` / `chopper` / API client packages + +These are runtime libraries with no code generation — `lib/`-vs-`source/` doesn't apply to the package itself. But the *Dart files you write to consume them* (your `ApiClient` class, your DTOs) go under `source/`. + +## What if the new package isn't listed here? + +Apply the rule of thumb: anywhere the docs say `lib/`, substitute `source/`. The package itself stays normal in `pubspec.yaml`. If it's a code generator, make sure `source/**` is in `build.yaml` sources. diff --git a/skills/solid/references/troubleshooting.md b/skills/solid/references/troubleshooting.md index 9d5f487..e7bb41f 100644 --- a/skills/solid/references/troubleshooting.md +++ b/skills/solid/references/troubleshooting.md @@ -8,9 +8,10 @@ Common errors and fixes. Source: plus the "commo | --- | --- | --- | | Edits to a file under `lib/` keep disappearing on save | `lib/` is generated; `build_runner watch` rewrites it from `source/`. | Move the change to the matching `source/.dart`. | | `lib/.dart` not produced | `source/**` not in `build.yaml` `targets.$default.sources`. | Add `- source/**` to the sources list. | -| Generator errors complain about stale outputs | Old `.g.dart` / `lib/` files conflict with regeneration. | `dart run build_runner build --delete-conflicting-outputs` (or `watch ...`). | +| Generator errors complain about stale outputs | Old `.g.dart` / `lib/` files conflict with regeneration. | `dart run build_runner build --delete-conflicting-outputs` (or `watch ...`). Or run `scripts/verify.sh`. | | `flutter run` doesn't pick up a `build_runner` rewrite | No IDE save event fires for filesystem changes. | Press `r` in the `flutter run` terminal, or use [`dashmonx`](https://pub.dev/packages/dashmonx) (`dashmonx -d chrome` etc.). | | Working with another generator (`freezed`, `json_serializable`, …) and only Solid runs | `build.yaml` `sources` list missing `lib/**` or `$package$`. | Use the full block: `- lib/**`, `- $package$`, `- source/**`. Run `dart run build_runner watch --delete-conflicting-outputs` once. | +| Generated `lib/` files are missing `const`, have unused imports, or use absolute `package:` imports | The generator prioritises correctness over polish; lint-driven fixes aren't applied. | Run `dart fix --apply` after `build_runner` — or use `scripts/verify.sh` which chains them. Apply in CI too. | ## Annotation rejections @@ -21,6 +22,7 @@ Common errors and fixes. Source: plus the "commo | `late` field with `@SolidState` never gets a value | A `late` `@SolidState` field needs an initializer site (or to be assigned before first read). | Either initialize at declaration or assign before read; or make the type nullable. | | `@SolidQuery() Future fetchData(int id) async {...}` rejected | Queries cannot have parameters. | Move the input into a `@SolidState` field; the query auto-re-runs when it changes. See `references/patterns.md` §5. | | `@SolidEnvironment() Counter counter;` errors at first read | Lookup is lazy and needs `late`. | Mark the field `late`: `@SolidEnvironment() late Counter counter;`. | +| Generator rejects `import 'package:/foo.dart';` from a `source/` file | Same-package imports inside `source/` must be relative — `package:` resolves to the generated `lib/` realm. | Use a relative import: `import '../path/to/foo.dart';`. | ## Runtime @@ -37,3 +39,4 @@ Common errors and fixes. Source: plus the "commo | --- | --- | --- | | `must_be_immutable` lint fires on every Solid widget | The `StatelessWidget` you write holds mutable fields. The generated widget is immutable. | In `analysis_options.yaml`: `analyzer.errors.must_be_immutable: ignore`. | | `public_member_api_docs` lint fires everywhere in `source/` | Solid's recommended setup disables it. | In `analysis_options.yaml`: `linter.rules.public_member_api_docs: false`. | +| Lints complain about `package:/...` imports inside `source/` | Same-package imports must be relative. | Set `linter.rules.always_use_package_imports: false` and `linter.rules.prefer_relative_imports: true`. | diff --git a/skills/solid/scripts/scaffold-widget.sh b/skills/solid/scripts/scaffold-widget.sh index eb38248..35c06d4 100755 --- a/skills/solid/scripts/scaffold-widget.sh +++ b/skills/solid/scripts/scaffold-widget.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Scaffold a starter Solid widget under source/.dart. +# # Usage: scaffold-widget.sh [--state|--query|--env] +# # Run from the package root. Refuses to overwrite an existing file. set -eu @@ -34,7 +36,7 @@ fi mkdir -p source -# PascalCase -> snake_case (e.g. CounterPage -> counter_page) +# PascalCase -> snake_case (CounterPage -> counter_page). snake="$(printf '%s' "$name" \ | sed -E 's/([a-z0-9])([A-Z])/\1_\2/g; s/([A-Z]+)([A-Z][a-z])/\1_\2/g' \ | tr '[:upper:]' '[:lower:]')" diff --git a/skills/solid/scripts/verify.sh b/skills/solid/scripts/verify.sh index c798fb0..0401168 100755 --- a/skills/solid/scripts/verify.sh +++ b/skills/solid/scripts/verify.sh @@ -1,8 +1,14 @@ #!/usr/bin/env bash -# Run the Solid (build_runner) code generator and report PASS/FAIL. -# Run from the package root (the directory with pubspec.yaml). -# Exits 0 on success, non-zero on failure. On failure, prints the first -# [SEVERE] line so the agent can act on it without parsing the full log. +# Verify a Solid package: regenerate lib/ from source/, then apply lint-driven +# fixes. Run from the package root (the directory with pubspec.yaml). +# +# Exit code: +# 0 = build_runner succeeded (dart fix failure is non-fatal, reported as WARN) +# 1 = build_runner failed +# 2 = misuse (not a package root) +# +# On build_runner failure, prints the first [SEVERE] line so the caller can act +# on it without parsing the full log. set -u @@ -14,17 +20,26 @@ fi log="$(mktemp -t solid-verify.XXXXXX)" trap 'rm -f "$log"' EXIT -if dart run build_runner build --delete-conflicting-outputs >"$log" 2>&1; then - echo "PASS: build_runner generated lib/ from source/." - exit 0 +# Step 1: build_runner. This is the hard requirement. +if ! dart run build_runner build --delete-conflicting-outputs >"$log" 2>&1; then + echo "FAIL: build_runner returned non-zero." >&2 + first_severe="$(grep -m1 '\[SEVERE\]' "$log" || true)" + if [[ -n "$first_severe" ]]; then + echo "First error: $first_severe" >&2 + else + echo "Last 20 lines of output:" >&2 + tail -n 20 "$log" >&2 + fi + exit 1 fi -echo "FAIL: build_runner returned non-zero." >&2 -first_severe="$(grep -m1 '\[SEVERE\]' "$log" || true)" -if [[ -n "$first_severe" ]]; then - echo "First error: $first_severe" >&2 -else - echo "Last 20 lines of output:" >&2 - tail -n 20 "$log" >&2 +# Step 2: dart fix. This is polish; failure here doesn't fail the script. +if dart fix --apply >>"$log" 2>&1; then + echo "PASS: build_runner generated lib/ from source/, dart fix applied." + exit 0 fi -exit 1 + +echo "PASS: build_runner generated lib/ from source/." +echo "WARN: dart fix --apply failed (non-fatal). Last 10 lines of log:" >&2 +tail -n 10 "$log" >&2 +exit 0 From 9799885bf7ad21f4c3d5510fe9eda7a48ca555d3 Mon Sep 17 00:00:00 2001 From: Alexandru Mariuti Date: Wed, 13 May 2026 20:44:36 +0200 Subject: [PATCH 2/9] gitignore: exclude solid-workspace/ skill-creator run artifacts skill-creator's run_loop.py produces per-run workspaces (results.json, report.html, per-iteration logs) alongside the skill. These are ephemeral evaluation outputs, not artefacts to track in the repo. Ran 5 iterations of run_loop against skills/solid (sonnet-4.5, 20 trigger queries split 12 train / 8 test, 3 runs per query). All five iterations tied on test pass count (4/8). The optimizer picked the original hand-written description as the best by held-out score. No SKILL.md change needed; description already at the local optimum that loop could find. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4c4ddfd..76c4bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ lcov.info .firebase .fvmrc .supermaven + +# skill-creator run_loop / eval workspaces (ephemeral artifacts) +solid-workspace/ From 91910ec9a8b52f81918a9313f180b23f06475f2e Mon Sep 17 00:00:00 2001 From: Alexandru Mariuti Date: Wed, 13 May 2026 23:09:51 +0200 Subject: [PATCH 3/9] Apply PRIORITY-framed description to SKILL.md (run_loop best, 5/8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran nine description-optimization passes through skill-creator's run_loop.py against 20 trigger queries (12 train / 8 test split, held-out scoring). The clean ceiling on this query set is 4/8 due to Claude's tendency to handle simple Flutter intents without consulting a skill (the agentskills.io "undertrigger on one-step queries" mode). The PRIORITY framing breaks past the noise floor by reframing the skill as a precondition for safe Dart edits in Solid projects: - v1 (original "use whenever working on…", sonnet): 4/8 - v3 ("encodes knowledge Claude can't infer alone", opus): 5/8 noisy - v4 (REQUIRED + trigger-phrase listing, opus): 5/8 noisy - v5-v6 (hybrid / bulleted variants): 4/8 (dilution) - v7 (v3 verified with 5 runs/query): 4/8 (variance flushed out) - v8 (PRIORITY — read first, opus, 5 runs): 5/8 reliably ← chosen - v9 (v8 + annotation-contract emphasis): regressed to 4/8 The v8 description catches the "scaffold a new page that fetches…" query at 3/5 trigger rate, which no other framing reached. Final sentence retains the @SolidQuery-no-params / @SolidEnvironment-late hooks that v3 used to catch @SolidEnvironment, giving the description coverage on two distinct query types at once. Per-iteration logs in solid-workspace/ (gitignored). Full run_loop_v8.log was the best at 25/40 runs correct, recall 25%, precision 100%. --- skills/solid/SKILL.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/skills/solid/SKILL.md b/skills/solid/SKILL.md index 47cb556..2a24862 100644 --- a/skills/solid/SKILL.md +++ b/skills/solid/SKILL.md @@ -1,19 +1,21 @@ --- name: solid description: > - Use this skill whenever working on a Dart/Flutter project whose `pubspec.yaml` - declares `solid_annotations` or `solid_generator` — that is the authoritative - signal, even when the user never says "Solid". In Solid projects, the - `source/` directory is the source of truth and `lib/` is generated by - build_runner; every code edit goes under `source/`, never `lib/`. Trigger on: - adding any widget, page, controller, route, or model; reactive patterns - ("when X changes fetch Y", "debounce this query", "make this reactive"); - installing pub packages like go_router, freezed, riverpod, drift, - json_serializable (their docs say `lib/` — substitute `source/`); diagnosing - "my lib/ edits keep disappearing"; or any `@SolidState`, `@SolidEffect`, - `@SolidQuery`, `@SolidEnvironment`, or `.untracked` usage. Do NOT trigger for - SolidJS / Solid.js (unrelated JavaScript framework) or Flutter projects - without `solid_annotations`/`solid_generator` in pubspec.yaml. + PRIORITY — read this skill FIRST before writing Dart code when `pubspec.yaml` + declares `solid_annotations` or `solid_generator`. In these projects Flutter + conventions are inverted: `lib/` is build_runner output from `source/`; + editing `lib/` is destroyed on next build. Without this skill you WRITE TO + THE WRONG DIRECTORY and silently lose work. Use whenever ANY of these appear: + `solid_annotations`/`solid_generator` in pubspec; + `@SolidState`/`@SolidEffect`/`@SolidQuery`/`@SolidEnvironment`/`.untracked` + in the user's message or code; "my lib/ edits keep disappearing"; + "add/scaffold a widget/page/route/model/controller" in such a project; + reactive patterns (debounce, "when X changes fetch Y", "make reactive"); or + installing pub packages (go_router, freezed, riverpod, drift, + json_serializable) whose docs say `lib/` — substitute `source/`. Annotation + contract (`@SolidQuery` no params, `@SolidEnvironment` needs `late`) is + non-obvious. NOT for SolidJS/Solid.js or Flutter projects without those + packages. --- # Solid (Flutter) From 98f3e5d4e6636888ed34fd12caf45b381a5764d2 Mon Sep 17 00:00:00 2001 From: Alexandru Mariuti Date: Thu, 14 May 2026 00:01:21 +0200 Subject: [PATCH 4/9] Add AGENTS.md template + docs setup guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After 10 description-optimization iterations against the skill triggering loop, the empirical ceiling on the test query set is ~5/8 due to Claude's documented "undertrigger on simple queries" pattern (agentskills.io). Solo skill triggering can't reliably catch routine-looking Flutter intents like "scaffold a page that fetches /api/profile" even when the project's pubspec.yaml clearly declares solid_annotations. AGENTS.md sidesteps the trigger problem entirely. Most modern AI coding tools (Claude Code, Cursor, Codex, GitHub Copilot, Amp, OpenHands, …) auto-load AGENTS.md at session start for every interaction, with no trigger gate. Users who copy this template to their Solid app root get the inverted source/-vs-lib/ rule applied to every prompt, not just ones that mention "Solid" or annotation names. The skill remains the on-demand reference (full annotation contract, references/, scripts, evals). AGENTS.md is the always-loaded baseline. Two-layer setup: AGENTS.md for the rule, skill for the depth. Docs updated to recommend AGENTS.md installation as Step 1 of AI assistant setup, with a curl one-liner and a CLAUDE.md symlink fallback. --- .../content/docs/guides/getting-started.mdx | 14 ++- skills/solid/assets/AGENTS.md | 91 +++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 skills/solid/assets/AGENTS.md diff --git a/docs/src/content/docs/guides/getting-started.mdx b/docs/src/content/docs/guides/getting-started.mdx index 4103755..787f50e 100644 --- a/docs/src/content/docs/guides/getting-started.mdx +++ b/docs/src/content/docs/guides/getting-started.mdx @@ -70,10 +70,18 @@ import { Steps } from '@astrojs/starlight/components'; ``` -