@danieljvdm/dev-kit 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +161 -68
  2. package/dev-kit.example.jsonc +8 -3
  3. package/package.json +19 -15
  4. package/schema/dev-kit.schema.json +52 -0
  5. package/skill-sources.jsonc +8 -12
  6. package/skill-sources.lock.json +3 -9
  7. package/skills/dev-kit/SKILL.md +74 -24
  8. package/skills/effect-atom-data-fetching/SKILL.md +40 -0
  9. package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
  10. package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
  11. package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
  12. package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
  13. package/skills/effect-atom-data-fetching/references/testing.md +63 -0
  14. package/skills/effect-ts/agents/openai.yaml +0 -1
  15. package/skills/effect-ts/references/audit-services.md +11 -11
  16. package/skills/effect-ts/references/guide-effect.md +56 -69
  17. package/skills/effect-ts/references/guide-error-handling.md +64 -73
  18. package/skills/effect-ts/references/guide-layers.md +187 -215
  19. package/skills/effect-ts/references/guide-observability.md +91 -116
  20. package/skills/effect-ts/references/guide-retries.md +32 -44
  21. package/skills/effect-ts/references/guide-schedule.md +26 -40
  22. package/skills/effect-ts/references/guide-schema.md +50 -57
  23. package/skills/effect-ts/references/guide-sql.md +47 -50
  24. package/skills/effect-ts/references/guide-testing.md +96 -98
  25. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
  26. package/skills/effect-ts/references/version-and-source.md +0 -1
  27. package/src/bin/dev-kit.ts +61 -28
  28. package/src/catalog-manager.ts +86 -34
  29. package/src/catalog.ts +72 -34
  30. package/src/cli-ui.ts +20 -16
  31. package/src/effect-source.ts +49 -19
  32. package/src/effect-tsgo.ts +66 -35
  33. package/src/gitignore.ts +19 -6
  34. package/src/index.ts +12 -0
  35. package/src/manifest.ts +51 -3
  36. package/src/node-symbolic-link.ts +3 -0
  37. package/src/oxlint-plugin-effect.js +3 -0
  38. package/src/oxlint-plugin-style.d.ts +8 -0
  39. package/src/oxlint-plugin-style.js +8 -0
  40. package/src/oxlint.js +14 -0
  41. package/src/oxlint.ts +14 -0
  42. package/src/package-skill-source.ts +190 -75
  43. package/src/path-digest.ts +37 -10
  44. package/src/project-package.ts +59 -0
  45. package/src/project-process-lock.ts +19 -12
  46. package/src/project-state.ts +29 -2
  47. package/src/skill-manager.ts +134 -55
  48. package/src/skill-selector.ts +8 -2
  49. package/src/source-manifest.ts +2 -6
  50. package/src/sync.ts +491 -121
  51. package/src/vendor.ts +112 -42
  52. package/src/vite-plus-hooks.ts +174 -0
  53. package/src/vite-plus-quality.ts +49 -0
  54. package/templates/AGENTS.md +9 -0
  55. package/templates/vite-plus/github-actions-check.yml +44 -0
  56. package/templates/vite-plus/vite.config.ts +22 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dev-kit
3
- description: Dev-kit operations for projects that configure dev-kit.jsonc, sync portable skills, run plan/apply or locked postinstalls, maintain dev-kit.lock.json, resolve ownership conflicts, patch managed ignores, or enable Effect TypeScript-Go.
3
+ description: Dev-kit operations for projects that configure dev-kit.jsonc, sync portable skills, run plan/apply or automatic postinstalls, perform locked CI checks, maintain dev-kit.lock.json, resolve ownership conflicts, patch managed ignores, or enable Effect TypeScript-Go.
4
4
  ---
5
5
 
6
6
  # Dev Kit
@@ -28,7 +28,7 @@ stores explicit skill names and exact commit/content digests.
28
28
  repository actually uses; do not infer capabilities from a product or
29
29
  company name alone.
30
30
  2. Run `dev-kit list --all`, then use `dev-kit search <terms>` and `dev-kit info
31
- <skill>` for each capability in the inventory. Compare every candidate's
31
+ <skill>` for each capability in the inventory. Compare every candidate's
32
32
  trigger description with concrete repository evidence. Keep explicitly
33
33
  requested creative or advisory skills even when they have no mechanical
34
34
  dependency signal.
@@ -51,8 +51,12 @@ stores explicit skill names and exact commit/content digests.
51
51
  6. Resolve conflicts, then run `dev-kit apply`. Commit the manifest and
52
52
  regenerated `dev-kit.lock.json`; keep `.dev-kit/` local. Finish when a second
53
53
  plan reports only unchanged resources and setup tasks.
54
- 7. Use `dev-kit apply --locked` in CI and the package lifecycle. Finish when a
55
- clean install converges from the committed manifest and lock.
54
+ 7. Use `dev-kit apply` in the package lifecycle so intentional dependency
55
+ upgrades regenerate owned outputs and `dev-kit.lock.json`. For strict CI,
56
+ either disable lifecycle scripts before `dev-kit apply --locked`, or run the
57
+ normal lifecycle and require the tracked working tree to remain clean. Never
58
+ run an unlocked apply before locked verification. Finish when a clean install
59
+ converges from the committed manifest and lock.
56
60
 
57
61
  ## Manifest
58
62
 
@@ -68,13 +72,18 @@ skill as `dev-kit` when project agents should carry the toolkit procedure.
68
72
  "include": ["dev-kit", "effect"],
69
73
  "exclude": [],
70
74
  "setup": {
71
- "claudeInstructions": { "enabled": true }
75
+ "agentInstructions": { "enabled": true },
76
+ "claudeInstructions": { "enabled": true },
77
+ "vitePlus": {
78
+ "hooks": { "enabled": true },
79
+ "quality": { "enabled": true },
80
+ },
72
81
  },
73
82
  "targets": {
74
83
  "agents": { "enabled": true, "mode": "copy" },
75
84
  "claude": { "enabled": true, "mode": "symlink" },
76
- "opencode": { "enabled": false, "mode": "symlink" }
77
- }
85
+ "opencode": { "enabled": false, "mode": "symlink" },
86
+ },
78
87
  }
79
88
  ```
80
89
 
@@ -83,11 +92,36 @@ use symlinks for additional harness discovery paths. Keep every target path
83
92
  project-relative and separate from the manifest, lock, state, and process-lock
84
93
  paths.
85
94
 
95
+ Enable `setup.agentInstructions` to manage a project-root `AGENTS.md` wrapper
96
+ that points agents back to this skill. When `vite-plus` is a declared direct
97
+ dependency, dev-kit includes its installed agent instructions in the wrapper.
86
98
  Enable `setup.claudeInstructions` when Claude Code should consume the same
87
- project-root instructions as Codex. It manages `CLAUDE.md` as a relative
88
- symlink to an existing `AGENTS.md`. Preserve any conflicting `CLAUDE.md`; when
89
- disabled, dev-kit removes only an unchanged link recorded in local ownership
90
- state.
99
+ project-root instructions; it manages `CLAUDE.md` as a relative symlink to the
100
+ wrapper or to an existing regular `AGENTS.md`. Preserve conflicting paths;
101
+ when disabled, dev-kit removes only unchanged outputs recorded in local
102
+ ownership state.
103
+
104
+ Enable `setup.vitePlus.hooks` when an installed direct `vite-plus` dependency
105
+ should manage Git hooks. Each apply checks the local `.vite-hooks/_` dispatcher,
106
+ the portable `.vite-hooks/pre-commit` hook, and `core.hooksPath`, then runs the
107
+ project-local `vp config --no-agent` when they need convergence. This recreates
108
+ ignored dispatchers in linked worktrees. Preserve other hook managers; Dev Kit
109
+ refuses to replace an unrelated `core.hooksPath`. Use `VITE_GIT_HOOKS=0` or
110
+ `HUSKY=0` to skip hook setup for an invocation.
111
+
112
+ Enable `setup.vitePlus.quality` only in supported Vite+/Effect repositories
113
+ that explicitly want Dev Kit to own the canonical root `vite.config.ts` and
114
+ `.github/workflows/check.yml`. It requires direct Dev Kit, Vite+, Effect,
115
+ Effect TypeScript-Go, and native TypeScript dependencies, as well as
116
+ `setup.effectTsgo.enabled`. It refuses custom destination files and conflicting
117
+ `check` or `typecheck` package scripts. Exact canonical files can be adopted;
118
+ later updates and cleanup occur only while the owned files remain unchanged.
119
+
120
+ The managed Vite config composes the shared formatter and linter presets,
121
+ configures `vp staged`, and defines cached `check` and `typecheck` Vite tasks.
122
+ The check task and GitHub Actions workflow run `vp fmt --check`, `vp lint`,
123
+ `vp test`, and finally `vp run typecheck` so Effect diagnostics come from the
124
+ patched native compiler.
91
125
 
92
126
  ## Ownership and conflicts
93
127
 
@@ -118,11 +152,17 @@ For one lifecycle entry point, configure:
118
152
  ```jsonc
119
153
  {
120
154
  "scripts": {
121
- "postinstall": "dev-kit apply --locked"
122
- }
155
+ "postinstall": "dev-kit apply",
156
+ },
123
157
  }
124
158
  ```
125
159
 
160
+ This intentionally refreshes the committed lock and owned outputs when the
161
+ package manager installs a new Dev Kit or selected package-skill version.
162
+ Review and commit those changes with the dependency update. Keep
163
+ `dev-kit apply --locked` as a verification command, not the normal local
164
+ lifecycle; in CI, run it only before any unlocked apply.
165
+
126
166
  ## Effect source checkout
127
167
 
128
168
  Enable the source task when agents should have canonical source matching the
@@ -131,8 +171,8 @@ installed Effect package:
131
171
  ```jsonc
132
172
  {
133
173
  "setup": {
134
- "effectSource": { "enabled": true }
135
- }
174
+ "effectSource": { "enabled": true },
175
+ },
136
176
  }
137
177
  ```
138
178
 
@@ -151,8 +191,8 @@ Enable the setup task in the same manifest:
151
191
  ```jsonc
152
192
  {
153
193
  "setup": {
154
- "effectTsgo": { "enabled": true }
155
- }
194
+ "effectTsgo": { "enabled": true },
195
+ },
156
196
  }
157
197
  ```
158
198
 
@@ -191,7 +231,16 @@ Use `lint.extends` instead of spreading the object so Vite+ composes nested
191
231
  rule maps correctly. Oxfmt has no inheritance mechanism, so spread its object
192
232
  before project-local options. Standalone `oxlint.config.ts` uses the same
193
233
  `extends: [recommendedOxlintConfig]`; standalone `oxfmt.config.ts` spreads the
194
- same `recommendedOxfmtConfig`.
234
+ same `recommendedOxfmtConfig`. The shared lint preset enables `typeAware` for
235
+ semantic lint rules but leaves `typeCheck` disabled. Effect TypeScript-Go
236
+ projects must run the patched native compiler separately with
237
+ `vp run typecheck` after `vp fmt --check`, `vp lint`, and `vp test`; Oxlint's
238
+ bundled `tsgolint` does not use the Effect patch.
239
+
240
+ The Oxlint preset enables the fixable
241
+ `stylistic/padding-line-between-statements` rule. It keeps adjacent variable
242
+ declarations grouped, requires a blank line before the next logical statement,
243
+ and separates every `return` statement from the preceding statement.
195
244
 
196
245
  The Oxlint preset registers Dev Kit's shared Effect plugin as `effect`, but
197
246
  does not enable its scope-sensitive rules globally. Effect projects should
@@ -204,9 +253,10 @@ in the consuming project.
204
253
 
205
254
  ## Current boundary
206
255
 
207
- Manage skill outputs, the `setup.claudeInstructions` link, the
208
- `setup.effectSource` checkout, and the explicit `setup.effectTsgo` task. Edit
209
- shared `package.json` and `tsconfig.json`
210
- contributions deliberately. The Oxlint and Oxfmt configurations are composable
211
- package exports, not manifest-managed outputs. Treat broader setup tasks as
212
- future manifest capabilities until the installed CLI exposes them.
256
+ Manage skill outputs, the `setup.agentInstructions` wrapper, the
257
+ `setup.claudeInstructions` link, the `setup.vitePlus.hooks` dispatcher, the
258
+ opt-in `setup.vitePlus.quality` config and GitHub workflow, the
259
+ `setup.effectSource` checkout, and the explicit `setup.effectTsgo` task.
260
+ Dependency and `tsconfig.json` contributions remain deliberate user-owned
261
+ edits. Custom Vite configs compose the Oxlint and Oxfmt package exports manually
262
+ and leave the canonical quality task disabled.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: effect-atom-data-fetching
3
+ description: Design, implement, debug, or review HTTP data fetching with Effect Atom in React applications. Use for AtomHttpApi.Service queries and mutations, parameterized Atom.family caches, RegistryProvider and runtime placement, TTL/SWR/polling behavior, reactivity-key invalidation, framework-appropriate SSR and focus handling, AsyncResult aggregation resets, and deterministic cache lifecycle tests.
4
+ ---
5
+
6
+ # Effect Atom Data Fetching
7
+
8
+ Model server data as stable atoms owned outside React renders. Give the application one intentional registry boundary and one compatible runtime factory; then choose cache retention, freshness, polling, and invalidation independently.
9
+
10
+ ## Workflow
11
+
12
+ 1. Inspect the installed `effect` and `@effect/atom-react` versions and their source before copying signatures. These APIs live under `effect/unstable/reactivity` and may move.
13
+ 2. Locate every `RegistryProvider`, runtime factory, query atom, mutation atom, and route-level `AsyncResult.all`. Draw the ownership boundary before changing behavior.
14
+ 3. Keep one `RegistryProvider` for the intended client application lifetime. Define the shared runtime factory, API service, query families, and mutation atoms at module scope rather than in components.
15
+ 4. Choose each lifecycle control for its actual job:
16
+ - idle `timeToLive`: retain an unused registry value before disposal;
17
+ - `Atom.swr({ staleTime })`: decide when mount/focus revalidation is needed;
18
+ - `Atom.withRefresh`: force periodic refresh while mounted.
19
+ 5. Give queries stable identities and matching reactivity keys. Let successful mutations invalidate those keys rather than manually coordinating every consumer.
20
+ 6. Keep browser-only signals behind an SSR-safe boundary. Decide explicitly whether initial data is client-only or hydrated from a request-scoped server registry.
21
+ 7. Verify lifecycle behavior with fake time and request counters, not sleeps.
22
+
23
+ ## Ownership rules
24
+
25
+ - Treat a query as shared read state: export one atom or `Atom.family` and let components subscribe.
26
+ - Treat an action as an event owned by the initiating UI or workflow: export the mutation atom, invoke it with `useAtomSet`, and observe its result only where useful.
27
+ - Never allocate a query atom in render. For parameterized queries, use a stable scalar or Effect `Hash`/`Equal` value as the family argument.
28
+ - Ensure every input to a route-level `AsyncResult.all` has stable atom identity and compatible retention. One newly allocated or immediately evicted input returns to `Initial` and makes the whole aggregate appear reset even when the other inputs remain cached.
29
+ - Do not describe manual refresh or polling as freshness caching. Refresh is forceful; `staleTime` only gates SWR's automatic mount/focus decisions.
30
+
31
+ ## References
32
+
33
+ - Read `./references/cache-lifecycle.md` for registry scope, runtime memoization, families, TTL, SWR, polling, and the aggregation trap.
34
+ - Read `./references/http-and-invalidation.md` for `AtomHttpApi.Service`, query/mutation ownership, reactivity-key vocabulary, and invalidation.
35
+ - Read `./references/tanstack-start.md` only for its framework-specific provider placement, SSR isolation, hydration, and focus guidance.
36
+ - Read `./references/testing.md` when adding or diagnosing lifecycle tests.
37
+
38
+ ## Completion check
39
+
40
+ Confirm one registry boundary, stable atom identity, deliberate TTL/staleness/polling values, matching query and mutation keys, SSR-safe browser access, and tests for every lifecycle behavior changed.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Effect Atom Data Fetching"
3
+ short_description: "Cache HTTP data safely with Effect Atom"
4
+ default_prompt: "Use $effect-atom-data-fetching to design or debug Effect Atom HTTP queries, caching, polling, invalidation, and SSR integration in a React app."
@@ -0,0 +1,72 @@
1
+ # Cache lifecycle
2
+
3
+ ## Registry and runtime scope
4
+
5
+ `RegistryProvider` creates one `AtomRegistry` on its first render. Its options do not rebuild that registry later. Provider unmount schedules disposal after a short grace period, so a quick React remount can reuse the same registry; moving or keying the provider still changes the cache boundary.
6
+
7
+ Place one provider around the client application subtree that should share data. Nested or route-local providers create separate caches.
8
+
9
+ An atom runtime and a registry solve different problems:
10
+
11
+ - the registry stores atom nodes, values, subscriptions, idle timers, and finalizers;
12
+ - `Atom.context({ memoMap })` creates runtimes that share `Layer` construction through one `Layer.MemoMap`;
13
+ - the module-level `Atom.runtime` uses Effect's module-level default memo map.
14
+
15
+ Create one client runtime factory when several API/services must share layers:
16
+
17
+ ```ts
18
+ import { Layer } from "effect";
19
+ import { Atom } from "effect/unstable/reactivity";
20
+
21
+ export const appAtomRuntime = Atom.context({
22
+ memoMap: Layer.makeMemoMapUnsafe(),
23
+ });
24
+ ```
25
+
26
+ Pass `appAtomRuntime` to each `AtomHttpApi.Service`. Do not create a memo map per query or component. On an SSR server, do not put request-specific authentication or services into a process-global memo map; use a request-scoped atom environment or keep the atom data path client-only.
27
+
28
+ ## Stable identity and families
29
+
30
+ Export fixed queries directly. Use `Atom.family` when a parameter selects the resource:
31
+
32
+ ```ts
33
+ export const projectAtom = Atom.family((projectId: string) =>
34
+ ApiClient.query("projects", "get", {
35
+ params: { projectId },
36
+ timeToLive: "5 minutes",
37
+ reactivityKeys: { projects: [projectId] },
38
+ }).pipe(Atom.swr({ staleTime: "30 seconds", revalidateOnMount: true })),
39
+ );
40
+ ```
41
+
42
+ The family must receive a stable key. Prefer a primitive ID. If the key is an object, give it deliberate Effect `Equal`/`Hash` semantics or reuse the same object; repeated object literals can produce distinct family entries.
43
+
44
+ ## Three independent clocks
45
+
46
+ | Control | Clock starts | What happens | What it does not mean |
47
+ | ----------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
48
+ | Registry/default idle TTL or query `timeToLive` | When an atom becomes unused | The registry keeps the cached node until idle eviction | The value is fresh during that period |
49
+ | `Atom.swr({ staleTime })` | From the latest success timestamp | A stale value can revalidate automatically on mount or focus while the prior success remains visible | The node survives an unmount long enough to be reused |
50
+ | `Atom.withRefresh(interval)` | While the wrapper is mounted | A timer force-refreshes the source and is canceled on disposal | Fresh requests are skipped |
51
+
52
+ Set retention long enough for the navigation/remount reuse window. `staleTime` cannot rescue a source that idle eviction already removed. A common policy is a longer `timeToLive`, a shorter `staleTime`, and polling only on screens that truly need it.
53
+
54
+ Manual `registry.refresh`, `useAtomRefresh`, invalidation, and `Atom.withRefresh` are forceful. They do not consult SWR freshness. Polling stops when the polling wrapper's lifetime is disposed because its finalizer clears the timer; applying `keepAlive` to that wrapper intentionally keeps polling alive.
55
+
56
+ ## The `AsyncResult.all` route reset
57
+
58
+ `AsyncResult.all` returns the first non-success input. Therefore a route aggregate is only as reusable as its least-stable input:
59
+
60
+ ```ts
61
+ const routeDataAtom = Atom.make((get) =>
62
+ AsyncResult.all({
63
+ project: get(projectAtom("p-1")),
64
+ // Bad if created during render or rebuilt for every route visit:
65
+ preferences: get(makePreferencesAtom()),
66
+ }),
67
+ );
68
+ ```
69
+
70
+ If `preferences` is a fresh or evicted atom, it starts at `Initial`; the aggregate also looks initial even though `project` is cached. Fix the input's ownership and retention. Define a singleton/family atom outside render and give it a deliberate idle TTL. Memoizing only the `AsyncResult.all` call does not repair an unstable input atom.
71
+
72
+ `AsyncResult.all` also constructs a new success container. Keep aggregation inside a derived atom so the registry controls recomputation instead of rebuilding the container ad hoc in render.
@@ -0,0 +1,93 @@
1
+ # HTTP queries and invalidation
2
+
3
+ ## Build one API service
4
+
5
+ `AtomHttpApi.Service` generates a typed client, a runtime, query atoms, and mutation functions. Pass the shared runtime factory so API services share the intended `Layer.MemoMap`:
6
+
7
+ ```ts
8
+ import { FetchHttpClient } from "effect/unstable/http";
9
+ import { AtomHttpApi } from "effect/unstable/reactivity";
10
+ import { appAtomRuntime } from "./atom-runtime";
11
+ import { Api } from "./api";
12
+
13
+ export const ApiClient = AtomHttpApi.Service()("ApiClient", {
14
+ api: Api,
15
+ httpClient: FetchHttpClient.layer,
16
+ baseUrl: "/api",
17
+ runtime: appAtomRuntime,
18
+ });
19
+ ```
20
+
21
+ Compile examples against the installed Effect version; the reactivity and HTTP APIs are unstable.
22
+
23
+ ## Queries
24
+
25
+ `query(group, endpoint, request)` returns an `Atom<AsyncResult<...>>`. The service internally memoizes request keys with a family. A public `Atom.family` remains useful for expressing domain ownership with a simple, stable parameter and applying one cache policy.
26
+
27
+ Query options have separate roles:
28
+
29
+ - `timeToLive`: finite values apply idle TTL; infinity keeps the query alive;
30
+ - `reactivityKeys`: register the query for refresh after matching invalidation;
31
+ - `serializationKey`: make decoded-only results serializable for hydration; it is not the runtime cache key;
32
+ - `responseMode`: changes the response and error shape.
33
+
34
+ Never place secrets in `serializationKey`, URL state, hydration payloads, or client-visible layers.
35
+
36
+ ## Mutations and action ownership
37
+
38
+ Create mutation atoms once, then invoke them from the component or workflow that owns the action:
39
+
40
+ ```ts
41
+ export const updateProject = ApiClient.mutation("projects", "update");
42
+
43
+ // In the initiating component:
44
+ const mutate = useAtomSet(updateProject, { mode: "promise" });
45
+ await mutate({
46
+ params: { projectId },
47
+ payload: patch,
48
+ reactivityKeys: { projects: [projectId] },
49
+ });
50
+ ```
51
+
52
+ The mutation's `reactivityKeys` are invalidated only after the request succeeds. Failed mutations do not invalidate. Keep navigation, toasts, dialog closure, and optimistic UI at the action owner; keep shared server-state refresh in reactivity keys.
53
+
54
+ ## Use one key vocabulary
55
+
56
+ Array keys represent independent keys. Record keys support hierarchical broad-plus-entity invalidation:
57
+
58
+ ```ts
59
+ const listKeys = { projects: [] };
60
+ const detailKeys = { projects: [projectId] };
61
+
62
+ ApiClient.query("projects", "list", { reactivityKeys: listKeys });
63
+ ApiClient.query("projects", "get", { params: { projectId }, reactivityKeys: detailKeys });
64
+
65
+ // This mutation invalidates the broad namespace and this entity key.
66
+ await mutate({ params: { projectId }, payload: patch, reactivityKeys: detailKeys });
67
+ ```
68
+
69
+ Record semantics register the property name and each `property:id` combination. Consequently, `{ projects: [projectId] }` is hierarchical: it invalidates both the broad `projects` namespace and the specific `projects:projectId` key. Every record-form project query also subscribed to that broad namespace can refresh. Use this when an entity write may affect lists or aggregates.
70
+
71
+ For exact entity-only invalidation, use namespaced primitive array keys consistently instead:
72
+
73
+ ```ts
74
+ const projectKey = (id: string) => `project:${id}`;
75
+
76
+ const detailKeys = [projectKey(projectId)];
77
+ const collectionKeys = ["projects"];
78
+
79
+ ApiClient.query("projects", "get", { params: { projectId }, reactivityKeys: detailKeys });
80
+ await mutate({ params: { projectId }, payload: patch, reactivityKeys: detailKeys });
81
+ ```
82
+
83
+ Use `collectionKeys` for the collection query and for mutations that can change membership or ordering.
84
+
85
+ Standardize key constructors in one module when several endpoints share them; mismatched strings fail silently.
86
+
87
+ Choose invalidation breadth from the server write:
88
+
89
+ - invalidate an exact array-form entity key when only one detail changed;
90
+ - invalidate the collection key, or use hierarchical record keys, when list membership, ordering, totals, or filters can change;
91
+ - invalidate multiple record properties when a write affects related aggregates.
92
+
93
+ Do not both invalidate and manually refresh the same query unless two requests are intentional.
@@ -0,0 +1,69 @@
1
+ # TanStack Start integration
2
+
3
+ TanStack Start code is isomorphic by default, including route loaders. Treat every module used by a route as server-capable unless an explicit boundary says otherwise.
4
+
5
+ ## Choose an SSR strategy
6
+
7
+ Use one of these models deliberately:
8
+
9
+ ### Client-only atom data
10
+
11
+ - Put one `RegistryProvider` in the app/root component so client navigations share a registry.
12
+ - Render atom consumers that touch browser-only APIs inside `ClientOnly` from `@tanstack/react-router`.
13
+ - Accept that the fallback is the server-rendered state and fetching begins on the client.
14
+ - Keep the client runtime and API service as module singletons.
15
+
16
+ ```tsx
17
+ import { RegistryProvider } from "@effect/atom-react";
18
+ import { ClientOnly } from "@tanstack/react-router";
19
+
20
+ export function AppShell() {
21
+ return (
22
+ <RegistryProvider defaultIdleTTL={30_000}>
23
+ <ClientOnly fallback={<DashboardSkeleton />}>
24
+ <Dashboard />
25
+ </ClientOnly>
26
+ </RegistryProvider>
27
+ );
28
+ }
29
+ ```
30
+
31
+ ### SSR plus hydration
32
+
33
+ - Create the registry and any request-specific runtime/layers per request; never share user/auth state through a process-global registry or memo map.
34
+ - Give decoded query atoms deterministic `serializationKey` values.
35
+ - Mount/run the required serializable atoms on the server, dehydrate only the intended values, and pass them through the document safely.
36
+ - Create the client registry once, then hydrate matching atom identities before descendants consume them. Use `HydrationBoundary` where the installed React adapter supports it.
37
+ - Verify that server and client construct the same API service, family arguments, and serialization keys.
38
+
39
+ Prefer framework loaders/server functions when they already own SSR data. Do not build a second atom SSR cache merely to mirror loader data; seed atoms from the loader or keep atom fetching client-only.
40
+
41
+ ## Focus is browser-only
42
+
43
+ `Atom.windowFocusSignal` reads `window` and `document.visibilityState` when mounted. Do not mount it during SSR. Put focus-enabled consumers behind `ClientOnly`, or inject a no-op server signal and the browser signal on the client.
44
+
45
+ ```ts
46
+ const project = projectAtom(projectId).pipe(
47
+ Atom.swr({
48
+ staleTime: "30 seconds",
49
+ revalidateOnFocus: true,
50
+ focusSignal: Atom.windowFocusSignal,
51
+ }),
52
+ );
53
+ ```
54
+
55
+ `revalidateOnFocus: true` respects `staleTime`; `"always"` forces a request on every focus signal.
56
+
57
+ ## In-memory limits
58
+
59
+ Effect Atom's registry cache is in memory and scoped to that registry:
60
+
61
+ - a new tab, hard reload, server process, or newly created provider starts another cache;
62
+ - idle TTL evicts only unused atoms and is not a maximum-entry or byte-size bound;
63
+ - `keepAlive` and infinite TTL can grow memory with unbounded family keys;
64
+ - browser memory is not durable or shared across users/devices;
65
+ - hydration transfers a snapshot, not a persistent distributed cache.
66
+
67
+ For large or unbounded parameter spaces, use finite TTLs and avoid `keepAlive`. Put durable/shared caching at the HTTP, server, CDN, or database layer.
68
+
69
+ Primary TanStack references: [execution model](https://tanstack.com/start/latest/docs/framework/react/guide/execution-model) and [`ClientOnly`](https://tanstack.com/router/latest/docs/api/router/clientOnlyComponent).
@@ -0,0 +1,63 @@
1
+ # Deterministic lifecycle testing
2
+
3
+ Test cache policy below React first with `AtomRegistry.make()`. Add a React integration test only for provider placement, hook behavior, a browser-only SSR boundary, or hydration.
4
+
5
+ Use fake timers, a request counter, controllable Effects, and explicit mounts:
6
+
7
+ ```ts
8
+ const registry = AtomRegistry.make({ defaultIdleTTL: 1_000 });
9
+ const unmount = registry.mount(queryAtom);
10
+ const first = registry.get(queryAtom);
11
+
12
+ // Advance the Effect scheduler/microtasks as required by the installed version.
13
+ // Assert with AsyncResult predicates and request counts.
14
+
15
+ unmount();
16
+ ```
17
+
18
+ Avoid wall-clock sleeps. Flush Effect work with the repository's established `Effect.yieldNow`/test-clock pattern and advance the test runner's fake timers.
19
+
20
+ Prefer the installed test APIs over invented helpers or matchers. The upstream Effect tests use `assert(AsyncResult.isSuccess(result))`, `Effect.runPromise(Effect.yieldNow)`, `vitest.advanceTimersByTimeAsync(...)`, and the cleanup returned by `registry.mount(atom)`.
21
+
22
+ ## Required scenarios
23
+
24
+ ### Remount reuse
25
+
26
+ 1. Mount and resolve the query; assert request count `1`.
27
+ 2. Unmount, advance less than idle TTL, remount the same atom identity.
28
+ 3. Assert the cached success is immediately available and no request occurs while still fresh.
29
+
30
+ ### Stale refresh
31
+
32
+ 1. Resolve once through an SWR wrapper.
33
+ 2. Advance past `staleTime` but not idle TTL.
34
+ 3. Remount or emit the injected focus signal.
35
+ 4. Assert the previous success stays available with `waiting: true`, then a second success arrives and request count becomes `2`.
36
+ 5. Also prove a fresh mount/focus does not request.
37
+
38
+ ### TTL eviction
39
+
40
+ 1. Resolve and unmount.
41
+ 2. Advance to just before TTL; assert reuse.
42
+ 3. Advance to/after TTL and flush disposal; remount.
43
+ 4. Assert `Initial`/waiting behavior and a new request.
44
+
45
+ ### Polling cleanup
46
+
47
+ 1. Mount the `Atom.withRefresh` wrapper and resolve once.
48
+ 2. Advance one interval; assert one forced refresh.
49
+ 3. Unmount and advance several intervals.
50
+ 4. Assert the counter does not change. This catches leaked timers or accidental `keepAlive`.
51
+
52
+ ### Mutation invalidation
53
+
54
+ 1. Mount list and detail queries with explicit keys.
55
+ 2. Run a successful mutation with matching keys; assert only the intended queries refresh.
56
+ 3. Run a failed mutation; assert no invalidation.
57
+ 4. Assert cleanup removes invalidation handlers after query disposal.
58
+
59
+ ### Aggregate stability
60
+
61
+ Mount a route atom that uses `AsyncResult.all`, resolve every input, unmount, and remount inside the retention window. Assert the aggregate never returns to `Initial`. Then let one input expire and prove the aggregate reset is caused by that input, not the retained queries.
62
+
63
+ For runtime/layer tests, seed `runtime.layer` through `RegistryProvider initialValues` with a deterministic test layer. This replaces network services without changing the production atom graph.
@@ -2,4 +2,3 @@ interface:
2
2
  display_name: "Effect Expert"
3
3
  short_description: "Apply current Effect v4 patterns and tooling"
4
4
  default_prompt: "Use $effect-ts to apply the relevant Effect architecture, schema, service, error, testing, HTTP, SQL, observability, or CLI guidance."
5
-
@@ -44,17 +44,17 @@ Enumerate source and test files, then find:
44
44
 
45
45
  Record one row per discovered service or candidate:
46
46
 
47
- | Field | Question |
48
- | --- | --- |
49
- | Owner | Which module owns the capability's meaning? |
50
- | Contract | Where are its interface and tag? |
51
- | Construction | Does construction yield every runtime dependency? |
52
- | Production | Who owns the concrete implementation and Layer choice? |
53
- | Tests | Does it have an intentional and honest substitute strategy? |
54
- | Consumers | Are capabilities yielded or drilled as values? |
55
- | Requirements | Do requirements remain visible to the composition root? |
56
- | Type boundary | Who owns decoding, narrowing, and error translation? |
57
- | Verdict | Keep, deepen, relocate, merge, remove, or create? |
47
+ | Field | Question |
48
+ | ------------- | ----------------------------------------------------------- |
49
+ | Owner | Which module owns the capability's meaning? |
50
+ | Contract | Where are its interface and tag? |
51
+ | Construction | Does construction yield every runtime dependency? |
52
+ | Production | Who owns the concrete implementation and Layer choice? |
53
+ | Tests | Does it have an intentional and honest substitute strategy? |
54
+ | Consumers | Are capabilities yielded or drilled as values? |
55
+ | Requirements | Do requirements remain visible to the composition root? |
56
+ | Type boundary | Who owns decoding, narrowing, and error translation? |
57
+ | Verdict | Keep, deepen, relocate, merge, remove, or create? |
58
58
 
59
59
  Build a companion type-safety inventory using
60
60
  [`guide-type-safety-and-boundaries.md`](guide-type-safety-and-boundaries.md).