@danieljvdm/dev-kit 0.11.3 → 0.12.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 (74) hide show
  1. package/README.md +70 -76
  2. package/dev-kit.example.jsonc +0 -4
  3. package/package.json +10 -6
  4. package/schema/dev-kit.schema.json +1 -46
  5. package/skills/build-effect-apis/SKILL.md +77 -0
  6. package/skills/build-effect-apis/agents/openai.yaml +4 -0
  7. package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
  8. package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
  9. package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
  10. package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
  11. package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
  12. package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
  13. package/skills/build-effect-apis/references/shared-contracts.md +108 -0
  14. package/skills/build-effect-apis/references/tanstack-start.md +86 -0
  15. package/skills/build-effect-apis/references/verification.md +50 -0
  16. package/skills/dev-kit/SKILL.md +58 -46
  17. package/skills/effect-architecture-audit/SKILL.md +26 -0
  18. package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
  19. package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
  20. package/skills/effect-ts/SKILL.md +21 -256
  21. package/skills/effect-ts/agents/openai.yaml +3 -3
  22. package/skills/testing/SKILL.md +5 -0
  23. package/src/catalog-manager.ts +16 -17
  24. package/src/catalog.ts +71 -16
  25. package/src/effect-source.ts +46 -24
  26. package/src/effect-tsgo.ts +49 -24
  27. package/src/gitignore.ts +5 -5
  28. package/src/index.ts +0 -6
  29. package/src/manifest.ts +0 -34
  30. package/src/node-symbolic-link.ts +2 -2
  31. package/src/oxfmt.js +5 -0
  32. package/src/oxfmt.ts +5 -0
  33. package/src/oxlint.js +5 -0
  34. package/src/oxlint.ts +5 -0
  35. package/src/package-skill-source.ts +51 -59
  36. package/src/path-digest.ts +7 -7
  37. package/src/project-package.ts +8 -7
  38. package/src/project-process-lock.ts +17 -12
  39. package/src/project-state.ts +1 -1
  40. package/src/skill-manager.ts +16 -14
  41. package/src/skill-selector.ts +12 -0
  42. package/src/sync.ts +170 -120
  43. package/src/tool-ignore-patterns.js +9 -0
  44. package/src/tool-ignore-patterns.ts +15 -0
  45. package/src/vendor.ts +67 -61
  46. package/src/vite-plus-dependency.ts +10 -11
  47. package/src/vite-plus-hooks.ts +24 -14
  48. package/src/vite-plus-quality.ts +21 -172
  49. package/src/vite-plus.js +81 -0
  50. package/src/vite-plus.ts +102 -0
  51. package/templates/AGENTS.md +1 -1
  52. package/skills/effect-ts/UPSTREAM.md +0 -28
  53. package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
  54. package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
  55. package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
  56. package/skills/effect-ts/references/atom-testing.md +0 -67
  57. package/skills/effect-ts/references/audit-services.md +0 -144
  58. package/skills/effect-ts/references/features.md +0 -525
  59. package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
  60. package/skills/effect-ts/references/guide-cli.md +0 -107
  61. package/skills/effect-ts/references/guide-datetime.md +0 -72
  62. package/skills/effect-ts/references/guide-effect.md +0 -440
  63. package/skills/effect-ts/references/guide-error-handling.md +0 -565
  64. package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
  65. package/skills/effect-ts/references/guide-layers.md +0 -989
  66. package/skills/effect-ts/references/guide-observability.md +0 -746
  67. package/skills/effect-ts/references/guide-retries.md +0 -434
  68. package/skills/effect-ts/references/guide-schedule.md +0 -343
  69. package/skills/effect-ts/references/guide-schema.md +0 -664
  70. package/skills/effect-ts/references/guide-sql.md +0 -536
  71. package/skills/effect-ts/references/guide-testing.md +0 -532
  72. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
  73. package/skills/effect-ts/references/version-and-source.md +0 -86
  74. package/templates/vite-plus/vite.config.ts +0 -22
@@ -0,0 +1,78 @@
1
+ # Effect Atom lifecycle
2
+
3
+ Treat query atoms as shared read state and mutations as actions owned by an
4
+ initiating UI or workflow. Choose registry scope, atom identity, retention,
5
+ freshness, polling, and cancellation independently.
6
+
7
+ ## Registry and runtime scope
8
+
9
+ `RegistryProvider` creates its registry on first render; later option changes do
10
+ not rebuild it. Provider unmount schedules disposal after a short grace period,
11
+ while moving or keying the provider changes the cache boundary. Place one
12
+ provider around the application subtree that should share client state; nested
13
+ or route-local providers create separate caches.
14
+
15
+ The registry stores nodes, values, subscriptions, idle timers, and finalizers.
16
+ An Atom runtime supplies Effect services, and `Atom.context({ memoMap })` lets
17
+ several runtimes share layer construction. Create neither a registry nor a memo
18
+ map per query or render. Keep request-specific authentication out of a
19
+ process-global server memo map.
20
+
21
+ ## Stable identity and families
22
+
23
+ Export fixed queries directly. Use `Atom.family` when a parameter selects the
24
+ resource. Prefer a primitive family argument; give object keys deliberate Effect
25
+ `Equal`/`Hash` semantics or reuse the same object. Calling a family with a stable
26
+ primitive during render already returns its stable member; an extra `useMemo`
27
+ does not improve identity.
28
+
29
+ ## Three independent clocks
30
+
31
+ | Control | Clock starts | Effect | It does not mean |
32
+ | ----------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------- | -------------------------- |
33
+ | Registry/default idle TTL or query `timeToLive` | When the atom becomes unused | Retain the cached node until idle eviction | The value remains fresh |
34
+ | `Atom.swr({ staleTime })` | At the latest success | Revalidate stale data on configured mount/focus signals while showing the prior success | The node survives unmount |
35
+ | `Atom.withRefresh(interval)` | While its wrapper is mounted | Force periodic refresh until disposal | Fresh requests are skipped |
36
+
37
+ Set retention long enough for the intended navigation/remount reuse window.
38
+ `staleTime` cannot rescue an already evicted node. Manual refresh, invalidation,
39
+ and polling are forceful and do not consult SWR freshness. Applying `keepAlive`
40
+ to a polling wrapper intentionally keeps that polling lifetime alive.
41
+
42
+ Atom registries are in-memory caches, not durable or shared storage. New tabs,
43
+ hard reloads, processes, and providers have separate caches. Idle TTL is not a
44
+ maximum-entry bound; finite TTLs are important for unbounded family keys.
45
+
46
+ ## React mounts and action ownership
47
+
48
+ - `useAtomValue(atom)` subscribes for rendering;
49
+ - `useAtomSet(atom)` mounts a writable atom and returns a setter;
50
+ - `useAtom(atom)` subscribes and writes when one component owns both behaviors.
51
+
52
+ Use `useAtom` instead of pairing `useAtomValue` and `useAtomSet` for the same
53
+ atom in one component. The split form creates a value subscription plus a
54
+ separate mount and obscures ownership.
55
+
56
+ Unmount releases only that hook's subscription or mount. Registry eviction waits
57
+ for remaining consumers and idle TTL, then runs node finalizers. Component
58
+ unmount, registry eviction, and `Atom.Interrupt` are distinct events.
59
+
60
+ Use `Atom.Interrupt` for explicit cancellation. A cleanup write publishes an
61
+ interrupted `AsyncResult.Failure`; it is not passive unmount release, can run
62
+ during React Strict Mode replay, and can cancel work owned by another consumer.
63
+
64
+ Place a multi-request action in an owner that lives for the whole sequence. If
65
+ navigation or disconnect must not leave a partially completed fan-out, use a
66
+ stable workflow atom/service or one durable server-side command.
67
+
68
+ ## Keep aggregates stable
69
+
70
+ `AsyncResult.all` returns the first non-success input, so an aggregate is only
71
+ as reusable as its least-stable input. A newly allocated or evicted input
72
+ returns to `Initial` and makes the aggregate appear reset even while other
73
+ queries remain cached.
74
+
75
+ Define every input as a module-scoped singleton or stable family member with
76
+ compatible retention. Aggregate inside a derived atom so the registry owns
77
+ recomputation; memoizing only the `AsyncResult.all` call does not repair an
78
+ unstable input atom.
@@ -0,0 +1,74 @@
1
+ # Deterministic Effect Atom testing
2
+
3
+ Test cache policy below React first with `AtomRegistry.make()`. Add a React
4
+ integration test for provider placement, hook behavior, browser-only SSR
5
+ boundaries, hydration, or Strict Mode ownership.
6
+
7
+ Use fake timers, a request counter, controllable Effects, and explicit mounts:
8
+
9
+ ```ts
10
+ const registry = AtomRegistry.make({ defaultIdleTTL: 1_000 });
11
+ const unmount = registry.mount(queryAtom);
12
+ const first = registry.get(queryAtom);
13
+
14
+ // Flush Effect work and advance the test runner's fake timers.
15
+
16
+ unmount();
17
+ ```
18
+
19
+ Use the repository's established `Effect.yieldNow` or test-clock pattern rather
20
+ than wall-clock sleeps. Prefer installed APIs and predicates such as
21
+ `AsyncResult.isSuccess`; use the cleanup returned by `registry.mount(atom)`.
22
+
23
+ ## Remount reuse
24
+
25
+ 1. Mount and resolve the query; assert request count `1`.
26
+ 2. Unmount, advance less than idle TTL, and remount the same atom identity.
27
+ 3. Assert cached success is immediately available and no fresh request occurs.
28
+
29
+ ## Stale refresh
30
+
31
+ 1. Resolve once through an SWR wrapper.
32
+ 2. Advance past `staleTime` but not idle TTL.
33
+ 3. Remount or emit the injected focus signal.
34
+ 4. Assert the previous success remains with `waiting: true`, then a second
35
+ success arrives and request count becomes `2`.
36
+ 5. Prove a fresh mount or focus signal does not request.
37
+
38
+ ## TTL eviction
39
+
40
+ 1. Resolve and unmount.
41
+ 2. Advance to just before TTL and assert reuse.
42
+ 3. Advance through TTL, flush disposal, and 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 and assert one forced refresh.
49
+ 3. Unmount, advance several intervals, and assert the counter stops changing.
50
+
51
+ ## Mutation invalidation
52
+
53
+ 1. Mount list and detail queries with explicit keys.
54
+ 2. Run a successful mutation and assert only intended queries refresh.
55
+ 3. Run a failed mutation and assert nothing invalidates.
56
+ 4. Dispose the queries and assert invalidation handlers are removed.
57
+
58
+ ## Strict Mode and action ownership
59
+
60
+ Render the owner inside `StrictMode`. Prove development setup-cleanup replay
61
+ does not write `Atom.Interrupt` or publish an interrupted failure before an
62
+ explicit cancellation. For multi-request actions, unmount during the sequence
63
+ and prove the chosen policy. If the atom is shared, prove unmounting one
64
+ consumer does not cancel work still owned by another.
65
+
66
+ ## Aggregate stability
67
+
68
+ Mount a route atom using `AsyncResult.all`, resolve every input, unmount, and
69
+ remount inside the retention window. Assert the aggregate never returns to
70
+ `Initial`. Then expire one input and prove that input causes the reset.
71
+
72
+ For runtime/layer tests, seed `runtime.layer` through `RegistryProvider`
73
+ `initialValues` with a deterministic test layer. Replace network services
74
+ without changing the production atom graph.
@@ -0,0 +1,56 @@
1
+ # Standard runtime assembly
2
+
3
+ Use this branch for conventional Node/Bun processes, generated documentation,
4
+ and Web-standard handlers. Confirm platform package names against the installed
5
+ versions.
6
+
7
+ ## Compose routes before choosing the runtime
8
+
9
+ ```ts
10
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
11
+ import { Layer } from "effect";
12
+ import { HttpRouter, HttpServer } from "effect/unstable/http";
13
+ import { HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi";
14
+ import { createServer } from "node:http";
15
+ import { ApplicationApi } from "@app/domain/http";
16
+
17
+ const ApplicationHttpLive = Layer.mergeAll(ProjectsHandlersLive, AuthenticateLive).pipe(
18
+ Layer.provide(ApplicationServicesLive),
19
+ );
20
+
21
+ const ApiRoutes = HttpApiBuilder.layer(ApplicationApi, {
22
+ openapiPath: "/openapi.json",
23
+ }).pipe(Layer.provide(ApplicationHttpLive));
24
+
25
+ const DocsRoute = HttpApiScalar.layer(ApplicationApi, { path: "/docs" });
26
+ const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute);
27
+ ```
28
+
29
+ Build the typed API route and documentation route from the same contract. Merge
30
+ raw routes here only when the API schema cannot represent their transport, and
31
+ ensure they independently enforce equivalent identity and security behavior.
32
+
33
+ ## Run a long-lived server
34
+
35
+ ```ts
36
+ const HttpServerLive = HttpRouter.serve(AllRoutes).pipe(
37
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
38
+ );
39
+
40
+ Layer.launch(HttpServerLive).pipe(NodeRuntime.runMain);
41
+ ```
42
+
43
+ Use the matching platform server layer for Bun or another runtime. Keep the
44
+ platform choice at this composition root.
45
+
46
+ ## Export a Web handler
47
+
48
+ ```ts
49
+ export const { handler, dispose } = HttpRouter.toWebHandler(
50
+ AllRoutes.pipe(Layer.provide(HttpServer.layerServices)),
51
+ );
52
+ ```
53
+
54
+ Use the Web handler for serverless and framework adapters that accept standard
55
+ `Request` and `Response` values. Preserve and call `dispose` when the host has a
56
+ lifecycle hook; do not recreate the handler and its runtime for every request.
@@ -0,0 +1,174 @@
1
+ # Server handlers and middleware
2
+
3
+ Implement the shared contract with group handlers, application services, and
4
+ layers. Confirm names against the installed Effect version; examples here use
5
+ the Effect v4 `effect/unstable/httpapi` surface.
6
+
7
+ - [Implement thin group handlers](#implement-thin-group-handlers)
8
+ - [Declare security middleware in the shared contract](#declare-security-middleware-in-the-shared-contract)
9
+ - [Implement security middleware on the server](#implement-security-middleware-on-the-server)
10
+ - [Assemble layers at the runtime edge](#assemble-layers-at-the-runtime-edge)
11
+ - [Use response escape hatches deliberately](#use-response-escape-hatches-deliberately)
12
+
13
+ ## Implement thin group handlers
14
+
15
+ Resolve application services once in the group builder, then map every endpoint
16
+ identifier to one handler. Use `Effect.fn` for callbacks that return Effects.
17
+
18
+ ```ts
19
+ import { Effect } from "effect";
20
+ import { HttpApiBuilder } from "effect/unstable/httpapi";
21
+ import { ApplicationApi } from "@app/domain/http";
22
+ import { Projects } from "../services/Projects";
23
+
24
+ export const ProjectsHandlersLive = HttpApiBuilder.group(
25
+ ApplicationApi,
26
+ "projects",
27
+ Effect.fn("ProjectsApi.handlers")(function* (handlers) {
28
+ const projects = yield* Projects;
29
+
30
+ return handlers
31
+ .handle("listProjects", () => projects.list().pipe(Effect.orDie))
32
+ .handle("getProject", ({ params }) =>
33
+ projects
34
+ .get(params.projectId)
35
+ .pipe(Effect.catchReasons("ProjectsError", { ProjectNotFound: Effect.fail }, Effect.die)),
36
+ );
37
+ }),
38
+ );
39
+ ```
40
+
41
+ Handlers receive decoded `params`, `query`, `headers`, and `payload`. Check
42
+ cross-field invariants at this boundary when schemas cannot express them, then
43
+ call one application workflow. Keep persistence, transactions, retries, and
44
+ multi-service orchestration in services.
45
+
46
+ Reconcile the service error channel with the endpoint contract here. Use
47
+ `Effect.orDie` when an endpoint declares no service failures. Use
48
+ `Effect.catchReason`, `Effect.catchReasons`, or `Effect.unwrapReason` when an
49
+ application service wraps domain reasons: re-fail only reasons declared by the
50
+ endpoint and turn unexpected infrastructure or invariant failures into defects.
51
+ Finish when each handler's expected error type is a subset of the endpoint's
52
+ declared error schema.
53
+
54
+ ## Declare security middleware in the shared contract
55
+
56
+ Keep the middleware declaration in the shared contract package so server,
57
+ OpenAPI, and generated clients see the same requirement. Keep its implementation
58
+ out of that package.
59
+
60
+ ```ts
61
+ // packages/domain/src/http/authorization.ts
62
+ import { Context, Schema } from "effect";
63
+ import { HttpApiGroup, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi";
64
+
65
+ export class CurrentActor extends Context.Service<CurrentActor, { readonly id: string }>()(
66
+ "app/CurrentActor",
67
+ ) {}
68
+
69
+ export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
70
+ "Unauthorized",
71
+ { message: Schema.String },
72
+ { httpApiStatus: 401 },
73
+ ) {}
74
+
75
+ export class Authenticate extends HttpApiMiddleware.Service<
76
+ Authenticate,
77
+ {
78
+ readonly provides: CurrentActor;
79
+ readonly requires: never;
80
+ }
81
+ >()("app/Authenticate", {
82
+ requiredForClient: true,
83
+ security: { bearer: HttpApiSecurity.bearer },
84
+ error: Unauthorized,
85
+ }) {}
86
+
87
+ export const ProjectsApi = HttpApiGroup.make("projects")
88
+ .add(GetProjectEndpoint, ListProjectsEndpoint)
89
+ .middleware(Authenticate)
90
+ .prefix("/projects");
91
+ ```
92
+
93
+ `requiredForClient` makes the generated client require a matching client-side
94
+ middleware layer. The security declaration also owns credential decoding and
95
+ OpenAPI security metadata; do not manually decode an `Authorization` header for
96
+ this case.
97
+
98
+ ## Implement security middleware on the server
99
+
100
+ A security middleware implementation is keyed by the names in its `security`
101
+ object. It can use server-only services without leaking them into clients.
102
+
103
+ ```ts
104
+ // apps/api/src/http/Authenticate.ts
105
+ import { Effect, Layer, Redacted } from "effect";
106
+ import { Authenticate, CurrentActor, Unauthorized } from "@app/domain/http";
107
+ import { Actors } from "../services/Actors";
108
+
109
+ export const AuthenticateLive = Layer.effect(
110
+ Authenticate,
111
+ Effect.gen(function* () {
112
+ const actors = yield* Actors;
113
+
114
+ return Authenticate.of({
115
+ bearer: Effect.fn("Authenticate.bearer")(function* (httpEffect, { credential }) {
116
+ const actor = yield* actors
117
+ .authenticate(Redacted.value(credential))
118
+ .pipe(
119
+ Effect.catch(() => Effect.fail(new Unauthorized({ message: "Invalid bearer token" }))),
120
+ );
121
+
122
+ return yield* Effect.provideService(httpEffect, CurrentActor, actor);
123
+ }),
124
+ });
125
+ }),
126
+ );
127
+ ```
128
+
129
+ Use ordinary `HttpApiMiddleware.Service` implementations for request logging,
130
+ tenancy, correlation IDs, or other cross-cutting work that is not a declared
131
+ security scheme. Those implementations may access `HttpServerRequest`, but they
132
+ must decode raw headers, cookies, and external values before providing services.
133
+ When middleware uses `requires` and `provides`, confirm ordering from the
134
+ installed docs: the outer middleware that requires a service is attached before
135
+ the inner middleware that provides it.
136
+
137
+ Every error a middleware can return belongs in its `error` schema. Keep the
138
+ server layer separate from the shared declaration so importing the contract
139
+ never pulls secrets, repositories, or platform services into a client bundle.
140
+
141
+ ## Assemble layers at the runtime edge
142
+
143
+ ```ts
144
+ const ProjectsHandlersProvided = ProjectsHandlersLive.pipe(Layer.provide(ApplicationServicesLive));
145
+
146
+ const ApiLive = HttpApiBuilder.layer(ApplicationApi, {
147
+ openapiPath: "/api/openapi.json",
148
+ }).pipe(
149
+ Layer.provide(ProjectsHandlersProvided),
150
+ Layer.provide(AuthenticateLive),
151
+ Layer.provideMerge(HttpRuntimePrerequisitesLive),
152
+ );
153
+ ```
154
+
155
+ The exact HTTP platform/router/server layers vary by runtime. Keep that wiring
156
+ at the entrypoint, provide all group and middleware layers, and let the type
157
+ system expose missing requirements. Verify that the OpenAPI path composes with
158
+ any API prefix as intended.
159
+
160
+ For a conventional Node/Bun process, generated docs route, or serverless web
161
+ handler, read [runtime-assembly.md](runtime-assembly.md).
162
+
163
+ Transport-wide middleware such as CORS may wrap the final HTTP application.
164
+ Keep API middleware for contract-visible behavior and runtime HTTP middleware
165
+ for concerns that also apply to raw or non-API routes.
166
+
167
+ ## Use response escape hatches deliberately
168
+
169
+ Return typed success values and declared errors for ordinary endpoints. Use
170
+ `HttpServerResponse` when the boundary must pass through an upstream response,
171
+ redirect, set cookies, stream, or control a non-default success status or body.
172
+ Use Effect request and cookie APIs to parse boundary state. Reserve unsafe JSON
173
+ response constructors for values whose safety is established elsewhere; encode
174
+ ordinary typed 4xx failures through the endpoint's declared error schemas.
@@ -0,0 +1,108 @@
1
+ # Shared contracts
2
+
3
+ Use a runtime-neutral domain package as the protocol boundary shared by the
4
+ server and every typed client.
5
+
6
+ ## Package shape
7
+
8
+ ```text
9
+ packages/domain/
10
+ ├── package.json
11
+ └── src/
12
+ ├── models/
13
+ │ ├── ids.ts
14
+ │ ├── errors.ts
15
+ │ └── views.ts
16
+ └── http/
17
+ ├── endpoints/
18
+ │ ├── get-project.ts
19
+ │ └── update-project.ts
20
+ ├── authorization.ts
21
+ ├── project-api.ts
22
+ ├── application-api.ts
23
+ └── index.ts
24
+ ```
25
+
26
+ Export the root API, contract-visible middleware declarations, and any genuinely
27
+ reusable schemas. Keep middleware implementations, server handlers, React,
28
+ database clients, secrets, platform bindings, and runtime layers out of this
29
+ package. A workspace export map can expose `./http`, `./models`, and endpoint
30
+ subpaths when consumers need them.
31
+
32
+ ## Model the wire
33
+
34
+ Use schemas for every external value. Prefer reusable named classes for DTOs,
35
+ branded scalar schemas for identifiers, and serializable tagged errors with
36
+ HTTP status metadata for expected failures.
37
+
38
+ ```ts
39
+ import { Schema } from "effect";
40
+ import { HttpApiSchema } from "effect/unstable/httpapi";
41
+
42
+ export const ProjectId = Schema.String.pipe(Schema.brand("ProjectId"));
43
+ export type ProjectId = typeof ProjectId.Type;
44
+
45
+ export class ProjectDto extends Schema.Class<ProjectDto>("ProjectDto")({
46
+ id: ProjectId,
47
+ name: Schema.NonEmptyString,
48
+ }) {}
49
+
50
+ export class ProjectNotFoundError extends Schema.TaggedError<ProjectNotFoundError>()(
51
+ "ProjectNotFoundError",
52
+ { id: ProjectId },
53
+ ) {}
54
+
55
+ export const ProjectNotFound = ProjectNotFoundError.pipe(HttpApiSchema.status("NotFound"));
56
+ ```
57
+
58
+ Keep success schemas limited to successful values. Put every expected 4xx/5xx
59
+ failure in `error`; defects remain defects. Reuse a tuple of transport errors
60
+ only when the same set is genuinely standard across endpoints.
61
+
62
+ Make DTOs describe the wire exactly. If clients receive `id`, expose `id` even
63
+ when a persistence model uses `publicId`; map between them in a small boundary
64
+ function. Keep endpoint-specific request DTOs beside their endpoint and move a
65
+ DTO into shared models only when multiple contracts reuse the same concept.
66
+
67
+ ## Define one endpoint per file
68
+
69
+ An endpoint declares the complete request and response contract inline.
70
+
71
+ ```ts
72
+ import { HttpApiEndpoint } from "effect/unstable/httpapi";
73
+ import { ProjectDto, ProjectId, ProjectNotFound } from "../../models";
74
+ import { RequestHeaders } from "../headers";
75
+
76
+ export const GetProjectEndpoint = HttpApiEndpoint.get("getProject", "/projects/:projectId", {
77
+ params: { projectId: ProjectId },
78
+ headers: RequestHeaders,
79
+ success: ProjectDto,
80
+ error: ProjectNotFound,
81
+ });
82
+ ```
83
+
84
+ Use the operation identifier as a stable protocol name: handlers and generated
85
+ clients address it directly. Represent optional query values with the installed
86
+ version's optional schema operator, such as `Schema.optionalKey`.
87
+
88
+ If headers use `Schema.Class`, generated client input may require a constructed
89
+ class instance rather than a structurally similar object. Encode a representative
90
+ instance in a test so this contract stays explicit.
91
+
92
+ ## Compose groups and the root
93
+
94
+ ```ts
95
+ import { HttpApi, HttpApiGroup } from "effect/unstable/httpapi";
96
+ import { GetProjectEndpoint, UpdateProjectEndpoint } from "./endpoints";
97
+
98
+ export const ProjectsApi = HttpApiGroup.make("projects")
99
+ .add(GetProjectEndpoint, UpdateProjectEndpoint)
100
+ .prefix("/projects");
101
+
102
+ export const ApplicationApi = HttpApi.make("application").add(ProjectsApi).prefix("/api");
103
+ ```
104
+
105
+ Group and root files compose; endpoint files specify. Re-export through index
106
+ files so server and clients import the same `ApplicationApi` value. A contract
107
+ change is complete only after all affected handlers, callers, tests, and
108
+ OpenAPI output agree with it.
@@ -0,0 +1,86 @@
1
+ # TanStack Start integration
2
+
3
+ TanStack Start modules are isomorphic unless an explicit boundary says
4
+ otherwise. Choose one data-execution model before wiring Effect Atom.
5
+
6
+ ## Client-only atom data
7
+
8
+ Use this when identity, focus signals, or other dependencies require browser
9
+ globals.
10
+
11
+ 1. Mount one `RegistryProvider` around the application subtree whose client
12
+ navigations should share cache state.
13
+ 2. Put browser-dependent atom consumers behind `ClientOnly`.
14
+ 3. Keep the runtime, `AtomHttpApi.Service`, query families, and mutations as
15
+ client module singletons.
16
+ 4. Render a useful server fallback and accept that API fetching begins after
17
+ hydration.
18
+
19
+ ```tsx
20
+ import { RegistryProvider } from "@effect/atom-react";
21
+ import { ClientOnly, Outlet } from "@tanstack/react-router";
22
+
23
+ export function Root() {
24
+ return (
25
+ <RegistryProvider defaultIdleTTL={60_000}>
26
+ <ClientOnly fallback={<AppSkeleton />}>
27
+ <Outlet />
28
+ </ClientOnly>
29
+ </RegistryProvider>
30
+ );
31
+ }
32
+ ```
33
+
34
+ `Atom.windowFocusSignal`, `localStorage`, `window`, and `document` belong in
35
+ this client-only branch.
36
+
37
+ ## SSR or loader-owned data
38
+
39
+ Prefer TanStack loaders or server functions when they already own SSR data.
40
+ Pass loader data into the client state graph or keep the query loader-owned;
41
+ avoid maintaining two independent server caches for the same request.
42
+
43
+ When atom SSR is intentional:
44
+
45
+ - create the registry and request-specific runtime/layers per request;
46
+ - give serializable queries deterministic `serializationKey` values;
47
+ - mount or run only the intended serializable atoms on the server;
48
+ - dehydrate only intended values;
49
+ - create the browser registry once and hydrate matching atom identities before
50
+ descendants read them;
51
+ - use `HydrationBoundary` when the installed React adapter supports it.
52
+
53
+ Process-global registries or memo maps can leak request-specific authentication
54
+ and server state across users. Verify request isolation with concurrent SSR
55
+ tests.
56
+
57
+ ## Focus is browser-only
58
+
59
+ `Atom.windowFocusSignal` reads `window` and `document.visibilityState` when
60
+ mounted. Keep focus-enabled consumers behind `ClientOnly`, or inject a no-op
61
+ server signal and the browser signal on the client. `revalidateOnFocus: true`
62
+ respects `staleTime`; `"always"` forces a request on each focus signal.
63
+
64
+ ## Separate API development
65
+
66
+ When TanStack Start and the Effect API run as separate local processes, proxy a
67
+ stable prefix and WebSocket upgrades through the app dev server:
68
+
69
+ ```ts
70
+ export default defineConfig({
71
+ plugins: [tanstackStart(), react()],
72
+ server: {
73
+ proxy: {
74
+ "/api": {
75
+ target: "http://127.0.0.1:8787",
76
+ changeOrigin: true,
77
+ ws: true,
78
+ },
79
+ },
80
+ },
81
+ });
82
+ ```
83
+
84
+ Keep the client base URL aligned with proxy and production routing. If
85
+ credentials cross origins, configure CORS and cookie/header behavior at the API
86
+ edge and test the deployed topology, not only the same-origin development path.
@@ -0,0 +1,50 @@
1
+ # Verification
2
+
3
+ Test the contract spine at its seams. Use the repository's established Effect
4
+ test integration and command authority.
5
+
6
+ ## Contract tests
7
+
8
+ - Decode and re-encode representative request, success, and error values.
9
+ - Prove branded path/query/header values reject invalid wire input.
10
+ - Encode any `Schema.Class` headers exactly as generated clients receive them.
11
+ - Assert each expected error carries the intended HTTP status and body encoding.
12
+ - Compare or smoke-test generated OpenAPI when the public contract changes.
13
+
14
+ ## Server tests
15
+
16
+ - Build every changed group and fail the test if an endpoint handler is missing.
17
+ - Provide deterministic test layers for application services and middleware.
18
+ - Prove each handler exposes only errors declared by its endpoint; exercise
19
+ expected reason mapping and unexpected failure defects separately.
20
+ - Exercise success, declared failure, malformed input, and middleware rejection.
21
+ - Assert cross-field boundary invariants before the service workflow runs.
22
+ - Exercise raw routes separately and prove they enforce middleware-equivalent
23
+ identity/security rules.
24
+
25
+ Use the installed `HttpApiTest` or an in-memory `HttpClient` when available;
26
+ otherwise run the built HTTP application against representative requests. Avoid
27
+ mocking below the contract so heavily that request encoding and response
28
+ decoding are skipped.
29
+
30
+ ## Client tests
31
+
32
+ - Derive the client from the same root API used by the server.
33
+ - Provide every `requiredForClient` middleware with
34
+ `HttpApiMiddleware.layerClient` and assert that it transforms the request.
35
+ - Assert params, query, headers, payload, and expected errors at least once for
36
+ every changed request shape.
37
+ - For Atom clients, complete every applicable scenario routed through
38
+ `effect-atom-testing.md`; use a deterministic HTTP layer so request encoding,
39
+ invalidation, and lifecycle remain observable.
40
+
41
+ ## Completion matrix
42
+
43
+ Account for every changed endpoint across these columns:
44
+
45
+ | Contract | Middleware | Handler/service | Client/query/mutation | Tests |
46
+ | ------------------------------------------------ | -------------------------------- | -------------------------------- | ---------------------------------------------- | -------------------------------- |
47
+ | Params, query, headers, payload, success, errors | Scope, provided services, errors | Identifier, invariants, workflow | Typed call shape, identity, cache/invalidation | Round-trip and boundary behavior |
48
+
49
+ Completion means every changed endpoint has an entry in every applicable
50
+ column and the repository's formatter, linter, typechecker, and tests pass.