From 2368543bf58ee2522f2616c733cd3909a73562e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1lcan=20=C3=87ak=C4=B1r?= Date: Tue, 25 Aug 2026 23:03:49 +0300 Subject: [PATCH] docs: add the CLAUDE.md this package never had magic_devtools is the only package in the ecosystem with no project instructions at all, while magic's own CLAUDE.md points at it by name ("the dev-tooling adapters live in the sibling magic_devtools package"). Anybody sent here arrived with nothing to read but the README. Written from the source rather than from the sibling files: barrel list and their purposes from the four library docblocks, 14 enrichers and 5 watchers counted from the install bodies, CI gates from ci.yml, SDK floors from pubspec.yaml, the preview release boundary from preview_routes.dart. The load-bearing parts are the ones a reader gets wrong on their own: why the package exists at all (magic core keeps zero dependency on dusk and telescope, so a production app does not resolve an E2E driver), why the kDebugMode guard lives at the consumer call site and must never move inward, and why the two-phase install cannot be collapsed into one call. The rule for deciding which half new wiring belongs to is stated, because that is the question the next change here will actually ask. No .claude/rules/ yet. Five source files do not have a directory-spanning invariant that a root spec cannot hold, and a rule arrives with the subsystem it governs rather than ahead of it. --- CLAUDE.md | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1bb6c89 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,125 @@ +# CLAUDE.md + +Guidance for Claude Code working inside the `magic_devtools` repo. This file is the root spec; the +package is small enough that it has no path-scoped rules under `.claude/rules/` yet. + +## What this package is for, and why it is a package at all + +`magic_devtools` is the seam between `magic` and the dev tooling. It holds the adapters that wire +`fluttersdk_dusk` and `fluttersdk_telescope` into a running Magic app, plus a dev-only component +preview catalog. + +It exists so that `magic` core can keep ZERO dependency on dusk and telescope. Those adapters used to +live in magic; moving them out is what lets a production app depend on magic without dragging an E2E +driver and a runtime inspector into its resolution graph. Every consumer adds this package as a +**dev_dependency**, never a dependency. + +That is also why every install call is guarded by `kDebugMode` AT THE CALL SITE, in the consumer's +`main.dart`, and never inside a method here. Moving the guard inward defeats the release tree-shake and +pulls both tools into the production bundle, which is the one failure this package's whole shape is +arranged to prevent. + +## Stack + +Flutter package. Dart >=3.11.0, Flutter >=3.41.0. Runtime deps: `magic`, `fluttersdk_dusk`, +`fluttersdk_telescope`, `fluttersdk_wind`. No code generation. + +It is the only package in the ecosystem that sees magic, dusk, telescope and wind at once. That is a +capability, not an accident: dusk's frozen dependency contract forbids it from importing any of the +packages whose data it reports, so anything that has to join the four can only be assembled here. + +`fluttersdk_wind` is reached through magic's barrel, which re-exports it wholesale. Importing it +directly is flagged as an unnecessary import. + +## Commands + +| Command | When | +|---|---| +| `flutter test --coverage` | Default runner, and the CI gate. | +| `flutter analyze --no-fatal-infos` | CI gate. | +| `dart format --set-exit-if-changed .` | CI gate. Run it before pushing; a branch that never ran the formatter fails here and nowhere earlier. | +| `flutter pub get` | Resolve deps. | +| `dart pub publish --dry-run` | Pre-publish validation. | + +Local sibling development goes through the gitignored `pubspec_overrides.yaml`. The committed +`pubspec.yaml` stays on hosted caret constraints so a fork outside this workspace resolves on its own. + +## Barrels + +Four, and the split is the API: + +| Barrel | For | +|---|---| +| `lib/magic_devtools.dart` | `MagicDevtools`, the umbrella `installPre` / `installPost` pair. What a consumer imports unless it needs something narrower. | +| `lib/dusk.dart` | `MagicDuskIntegration` alone: 14 snapshot enrichers plus the `MagicRouter` navigate adapter. | +| `lib/telescope.dart` | `MagicTelescopeIntegration` alone: 5 Magic watchers plus `MagicHttpFacadeAdapter`. | +| `lib/preview.dart` | The dev-only component preview catalog. | + +Reach for a narrow barrel only when wiring one tool without the other, or when a host wants a +non-standard watcher set. + +## The two-phase install is a contract, not a convenience + +`installPre` runs BEFORE `Magic.init()`; `installPost` runs after. Neither half can move. + +- **Pre** boots `DuskPlugin` and `TelescopePlugin` and registers telescope's opt-in `ExceptionWatcher` + and `DumpWatcher`. It has to be early so the snapshot pipeline and the exception watcher are already + live while Magic boots: they capture boot-time errors and the first route resolve, which is exactly + the window a later install misses. +- **Post** wires Magic's runtime in. Its watchers, HTTP adapter and enrichers resolve dependencies + through the IoC container (`Magic.find` / `Magic.bound`), so `Magic.init()` must have completed. + +When adding wiring, decide which half it belongs to by asking what it resolves, not by which reads +tidier. Anything that touches the container is post. Anything that has to observe Magic booting, or +that registers with a subsystem which locks its table on first read (the router does), is pre. + +Every install here is idempotent, so a second call in the same isolate is safe. Keep it that way: a +consumer with a lazy debug toggle will call twice. + +## Enrichers and watchers + +An enricher is `String? Function(Element, RefRegistry)`, dusk's frozen typedef. It is synchronous and +stateless, and it must NEVER retain the `Element` across calls. `MagicDuskIntegration.uninstall()` +removes each enricher it added, one by one, so an enricher added to the install list without a matching +removal leaks into the next test. + +A watcher implements telescope's `TelescopeWatcher` contract (`name`, `install()`, `uninstall()`). +Dependency direction is one way and stays one way: `magic_devtools` depends on the telescope and dusk +contracts, and neither of those ever depends on magic. + +## Preview catalog + +`lib/preview.dart` hosts auto-discovered component previews behind `/preview` and `/preview/:component`. +It is reachable only through `MagicPreview.registerRoutes()`, which is guarded by `kReleaseMode` plus +`bool.fromEnvironment('PREVIEW_ENABLED')`, so it is tree-shaken from release builds. + +Registration happens in the consumer's `RouteServiceProvider.boot()`, and it has to: `MagicRouter` +locks its route table the first time `routerConfig` is read, and a registration after that point +throws. + +## Golden rules + +1. `flutter analyze` clean, `dart format` zero diff, `flutter test` green. All three are CI jobs, so + all three pass locally first. +2. TDD, red then green. A behaviour change gets a failing test that fails for the right reason before + the implementation. +3. `CHANGELOG.md` gets a bullet under `## [Unreleased]` for every behavioural or interface change. +4. Never add a dependency that would let magic core reach dusk or telescope. The direction is + `magic_devtools` to the tools, never the reverse. +5. Never move a `kDebugMode` guard inside this package. + +## Branching + +One long-lived `master`. Task branches (`feat/*`, `fix/*`, `docs/*`, `chore/*`) cut from master, PR +back into master. Release bumps `pubspec.yaml` and promotes `## [Unreleased]`, then a tag triggers +`publish.yml`. + +## Style + +- English only, in identifiers, comments, docblocks and commits. +- Types everywhere. Docblocks on every public class and contract, saying WHY rather than restating the + signature. +- Multi-line collections with trailing commas. 120-character lines. +- No em-dash or en-dash anywhere, including commits and PR bodies. Comma, colon, semicolon, period or + parentheses instead. +- No linter suppressions.