@craft-ts/mcp 0.7.0-beta.17 → 0.7.0-beta.18

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.
@@ -45,7 +45,7 @@ Decision page: `/guide/concepts/choose-primitive`.
45
45
  state.
46
46
  3. **Do not use `async` / `await` / `for await`.** Generators, `craftSleep`, and `CraftHttpClient` replace them.
47
47
  4. **HTTP:** `query` for reads, `mutation` for writes, both backed by `CraftHttpClient`. No raw `fetch` / `HttpClient`.
48
- 5. **Forms** derive from `state` + `insertForm`. Validators are `cRequired`, `cEmail`, `cMinLength`, … Submit through `insertFormSubmit` + a `mutation`. Failures are `craftException` values.
48
+ 5. **Forms** derive from `state` + `insertForm`. Validators are `cRequired`, `cEmail`, `cMinLength`, … Submit through `insertFormSubmit` + a `mutation`. Failures are `craftException` values. Search terms `form`, `formulaire`, `validation`, `submit`, `field`, and `FormData` map to these APIs; native `FormData` is an interoperability boundary, not the form state model.
49
49
  6. **Services:** `craftService({ name, scope }, function* () { ... })`. Consume
50
50
  the generated `X()` helper, typically `yield* X(...)`.
51
51
  7. **Routes:** `craftRoutes(name, [...])`, every component route has `componentDeps: {} as import('./x').GenDeps_X`, and **every file** has its own `ValidateCascadeRoutesFile` / `CanRun` check. Parent checks do not cover `loadChildren`. On `TS2589`, split with `loadChildren` — never delete the check.
@@ -7,12 +7,12 @@
7
7
  {
8
8
  "path": "/guide",
9
9
  "title": "Guide",
10
- "body": "# Guide\n\nThe guide is organised by **what you are trying to do**. If you are starting\nout, the [Learn path](/learn/) is a better entry point — it introduces the same\nmaterial one idea at a time.\n\n## Start here\n\nFour pages carry most of the weight. Reading them in this order is worth an\nafternoon:\n\n1. [The mental model](/guide/concepts/mental-model) — the principles the API\n follows and the guarantees they provide\n2. [Which primitive should I use?](/guide/concepts/choose-primitive) — the\n five-way decision you make constantly\n3. [Anatomy of a primitive](/guide/concepts/primitive-anatomy) — the shape all\n five share\n4. [Generators and `yield*`](/guide/concepts/generators) — the tracking channel\n everything is built on\n5. [Insertions](/guide/concepts/insertions) — how behaviour is composed\n\n## Project setup\n\n[Create a CraftTS project](/guide/create-project) — interactive and\nnon-interactive starters, configuration options, and first checks\n\n## By topic\n\n### Managing state\n\n[Local state](/guide/state/local-state) ·\n[State machines](/guide/state/state-machines) ·\n[query](/guide/state/server-state) ·\n[Mutations](/guide/state/mutations) ·\n[queryParams](/guide/state/url-state) ·\n[asyncProcess](/guide/state/async-process) ·\n[Collections](/guide/state/collections) ·\n[Persistence](/guide/state/persistence) ·\n[Selecting](/guide/state/select) ·\n[Reacting to mutations](/guide/state/react-on-mutation) ·\n[Schema validation](/guide/state/schema-validation)\n\n### Structuring the app\n\n[craftService](/guide/app/craft-service) ·\n[Service scopes](/guide/app/service-scopes) ·\n[Shaping the public API](/guide/app/expose-api) ·\n[Abstract services](/guide/app/abstract-services) ·\n[App start](/guide/app/app-start) ·\n[Lazy services](/guide/app/lazy-services)\n\n### Recommended approaches\n\n[Inject at the point of use](/guide/patterns/inject-at-point-of-use)\n\n### Routing and type-safe DI\n\n[Setup](/guide/routing/setup) ·\n[CLI automation](/guide/routing/automation) ·\n[ESLint rules](/guide/routing/eslint-rules) ·\n[Route providers](/guide/routing/route-providers) ·\n[Guards](/guide/routing/guards) ·\n[Exception handling](/guide/routing/exception-handling) ·\n[Pending UI](/guide/routing/pending-ui) ·\n[Route load errors](/guide/routing/route-load-errors) ·\n[Scaling routes](/guide/routing/scaling)\n\n### Components and templates\n\n[Components](/guide/components/) ·\n[Fine-grained reactivity](/guide/components/fine-grained-reactivity) ·\n[Progressive `forNode`](/guide/components/schedule-for) ·\n[Directives and `.pipe(...)`](/guide/components/directives) ·\n[Customization](/guide/components/customization) ·\n[Content projection](/guide/components/content-projection) ·\n[Encapsulated styles](/guide/components/styles) ·\n[Accessibility](/guide/components/accessibility)\n\n### Forms\n\n[Overview](/guide/forms/) ·\n[Validators](/guide/forms/validation) ·\n[Submitting](/guide/forms/submit) ·\n[Nested forms](/guide/forms/nested)\n\n### Testing\n\n[Services](/guide/testing/services) ·\n[Components](/guide/testing/components) ·\n[Type-level tests](/guide/testing/type-level) ·\n[Browser boundaries](/guide/testing/browser-boundaries) ·\n[Architecture rules](/guide/testing/architecture) ·\n[Craft graph vs Nx](/guide/testing/craft-graph-vs-nx)\n\n### Reactivity utilities\n\n[craftComputed](/guide/reactivity/craft-computed) ·\n[craftEffect](/guide/reactivity/craft-effect) ·\n[craftMethod](/guide/reactivity/craft-method) ·\n[source$](/guide/reactivity/source) ·\n[on$](/guide/reactivity/on)\n\n### Going further\n\n[SSR and hydration](/guide/advanced/ssr-hydration) ·\n[Program operators](/guide/advanced/program-operators) ·\n[Pattern matching](/guide/advanced/pattern-matching) ·\n[Observability](/guide/advanced/observability) ·\n[Live page MCP](/guide/ai/dev-page) ·\n[Coding agents](/resources/ai-agents)\n\n## Looking for one symbol?\n\nThe [API index](/reference/) lists every export with a one-line description.\n"
10
+ "body": "# Guide\n\nThe guide is organised by **what you are trying to do**. If you are starting\nout, the [Learn path](/learn/) is a better entry point — it introduces the same\nmaterial one idea at a time.\n\n## Start here\n\nFour pages carry most of the weight. Reading them in this order is worth an\nafternoon:\n\n1. [The mental model](/guide/concepts/mental-model) — the principles the API\n follows and the guarantees they provide\n2. [Which primitive should I use?](/guide/concepts/choose-primitive) — the\n five-way decision you make constantly\n3. [Anatomy of a primitive](/guide/concepts/primitive-anatomy) — the shape all\n five share\n4. [Generators and `yield*`](/guide/concepts/generators) — the tracking channel\n everything is built on\n5. [Insertions](/guide/concepts/insertions) — how behaviour is composed\n\n## Project setup\n\n[Create a CraftTS project](/guide/create-project) — interactive and\nnon-interactive starters, configuration options, and first checks\n\n## By topic\n\n### Managing state\n\n[Local state](/guide/state/local-state) ·\n[State machines](/guide/state/state-machines) ·\n[query](/guide/state/server-state) ·\n[Mutations](/guide/state/mutations) ·\n[queryParams](/guide/state/url-state) ·\n[asyncProcess](/guide/state/async-process) ·\n[Collections](/guide/state/collections) ·\n[Persistence](/guide/state/persistence) ·\n[Selecting](/guide/state/select) ·\n[Reacting to mutations](/guide/state/react-on-mutation) ·\n[Schema validation](/guide/state/schema-validation)\n\n### Structuring the app\n\n[craftService](/guide/app/craft-service) ·\n[Service scopes](/guide/app/service-scopes) ·\n[Shaping the public API](/guide/app/expose-api) ·\n[Abstract services](/guide/app/abstract-services) ·\n[App start](/guide/app/app-start) ·\n[Lazy services](/guide/app/lazy-services)\n\n[Server functions](/guide/app/server-functions)\n\n### Recommended approaches\n\n[Inject at the point of use](/guide/patterns/inject-at-point-of-use)\n\n### Routing and type-safe DI\n\n[Setup](/guide/routing/setup) ·\n[CLI automation](/guide/routing/automation) ·\n[ESLint rules](/guide/routing/eslint-rules) ·\n[Route providers](/guide/routing/route-providers) ·\n[Guards](/guide/routing/guards) ·\n[Exception handling](/guide/routing/exception-handling) ·\n[Pending UI](/guide/routing/pending-ui) ·\n[Route load errors](/guide/routing/route-load-errors) ·\n[Scaling routes](/guide/routing/scaling)\n\n### Components and templates\n\n[Components](/guide/components/) ·\n[Fine-grained reactivity](/guide/components/fine-grained-reactivity) ·\n[Progressive `forNode`](/guide/components/schedule-for) ·\n[Directives and `.pipe(...)`](/guide/components/directives) ·\n[Customization](/guide/components/customization) ·\n[Content projection](/guide/components/content-projection) ·\n[Encapsulated styles](/guide/components/styles) ·\n[Accessibility](/guide/components/accessibility)\n\n### Forms\n\n[Overview](/guide/forms/) ·\n[Validators](/guide/forms/validation) ·\n[Submitting](/guide/forms/submit) ·\n[Nested forms](/guide/forms/nested)\n\n### Testing\n\n[Services](/guide/testing/services) ·\n[Components](/guide/testing/components) ·\n[Type-level tests](/guide/testing/type-level) ·\n[Browser boundaries](/guide/testing/browser-boundaries) ·\n[Architecture rules](/guide/testing/architecture) ·\n[Craft graph vs Nx](/guide/testing/craft-graph-vs-nx)\n\n### Reactivity utilities\n\n[craftComputed](/guide/reactivity/craft-computed) ·\n[craftEffect](/guide/reactivity/craft-effect) ·\n[craftMethod](/guide/reactivity/craft-method) ·\n[source$](/guide/reactivity/source) ·\n[on$](/guide/reactivity/on)\n\n### Going further\n\n[SSR and hydration](/guide/advanced/ssr-hydration) ·\n[Program operators](/guide/advanced/program-operators) ·\n[Pattern matching](/guide/advanced/pattern-matching) ·\n[Observability](/guide/advanced/observability) ·\n[Live page MCP](/guide/ai/dev-page) ·\n[Coding agents](/resources/ai-agents)\n\n## Looking for one symbol?\n\nThe [API index](/reference/) lists every export with a one-line description.\n"
11
11
  },
12
12
  {
13
13
  "path": "/guide/advanced/effect",
14
14
  "title": "Using Effect with CraftTS",
15
- "body": "# Using Effect with CraftTS\n\nEffect belongs in CraftTS when the problem is a **domain program**: composing\nservices, modelling typed failures, controlling resources, or running an\noperation that crosses an I/O boundary. CraftTS remains responsible for\ncomponents, fine-grained rendering, reactive state and resource lifecycles.\n\nThe integration is deliberately a boundary, not a second UI runtime:\n\n```text\nCraft component / template\n ↓\nCraft primitive or generator\n ↓\nEffect<A, E, R>\n ↓\nLayer<R> provided by the Craft injector\n```\n\nIf a value is only local UI state, keep it in Craft. If it is a domain operation\nwith typed errors or services, define it as an Effect and adapt it at the Craft\nboundary.\n\n## The short decision\n\n| Need | Use | Why |\n| ------------------------------------------------ | -------------------------------- | ---------------------------------------------------------------- |\n| Toggle, draft, selection or other local UI value | `state` | Craft owns reactive UI state |\n| Read data with an Effect loader | `queryEffect` | loading, caching, cancellation and exceptions are Craft concerns |\n| Derive a reactive value from a synchronous Effect | `computedEffect` | runs a `SyncOp` Effect in place — a value, not a resource |\n| Expose a synchronous Effect as a callable method | `methodEffect` | the Effect counterpart of `craftMethod` |\n| Run a synchronous Effect in a lower-level position | `syncEffect` | `params`, a `craftMethod`, a `state` updater |\n| Write data with an Effect loader | `mutationEffect` | explicit writes and mutation reactions |\n| Run an explicit command | `asyncProcessEffect` | export, refresh, share action or other non-resource process |\n| Provide Effect services | `provideLayer` | app and route injectors own Layer scope |\n| Select a service from a Craft factory | `effectService` | records the Effect service dependency and selected members |\n| Yield one Effect in a Craft generator | `runEffect` | low-level bridge with typed Craft exceptions |\n| Validate data with Effect Schema | `Schema.toStandardSchemaV1(...)` | uses Craft's schema boundary without coupling core to Effect |\n\nThere is intentionally no `stateEffect`. A reactive value is not made better by\nbeing an Effect. Use `state` for the value, and use Effect for the computation\nthat loads or changes it.\n\n## Install the packages\n\n```shell\nnpm i @craft-ts/core@beta @craft-ts/component@beta @craft-ts/effect@beta\nnpm i effect@rc\n```\n\nKeep the three Craft packages on the same version. `@craft-ts/effect` declares\n`effect` as a peer dependency.\n\n## Install the bridge once\n\nThe bridge teaches Craft's generator driver how to execute a yielded Effect.\nInstall it at application bootstrap:\n\n```typescript\nimport { provideAppInitializer } from '@craft-ts/core';\nimport { installCraftEffectBridge } from '@craft-ts/effect';\n\nexport const appConfig = craftAppConfig({\n providers: [\n provideAppInitializer(() => {\n installCraftEffectBridge();\n }),\n ],\n});\n```\n\nIn a test, install it in `beforeEach` and call the returned disposer in\n`afterEach`. Do not install a new bridge in every loader or component.\n\n## Keep components in Craft\n\nA Craft component still has a generator factory and a typed template. The\ncomponent should call a domain operation, not resolve its repository or start a\nfiber from a click handler:\n\n```typescript\nimport { button, craftComponent, p } from '@craft-ts/component';\nimport { queryEffect } from '@craft-ts/effect';\nimport { loadUserProfile } from './profile-domain';\n\nexport const Profile = craftComponent(\n 'Profile',\n {},\n function* () {\n const profile = yield* queryEffect('profile', {\n params: () => 'user-ada',\n loader: ({ params }) => loadUserProfile(params),\n });\n\n return { profile };\n },\n ({ profile }) => [\n p(function* () {\n const user = yield* profile.value();\n return user?.name ?? 'Loading…';\n }),\n button(\n 'reload',\n {\n *click() {\n yield* profile.reload();\n },\n },\n 'Reload',\n ),\n ],\n);\n```\n\nThe template consumes Craft readers. It does not subscribe to an Effect, call\n`Effect.runPromise`, or convert a Promise into a signal manually.\n\n## Define the domain in Effect\n\nUse Effect for domain contracts and implementations. Tagged errors are values in\nthe `E` channel:\n\n```typescript\nimport { Context, Data, Effect, Layer } from 'effect';\n\nexport class UserNotFound extends Data.TaggedError('UserNotFound')<{\n readonly userId: string;\n}> {}\n\nexport type UserRepository = {\n readonly byId: (userId: string) => Effect.Effect<User, UserNotFound>;\n};\n\nexport class UserRepositoryService extends Context.Service<\n UserRepositoryService,\n UserRepository\n>()('app/UserRepository') {}\n\nexport const UserRepositoryLive = Layer.sync(UserRepositoryService)(() => ({\n byId: (userId) => findUserInDatabase(userId),\n}));\n\nexport function loadUserProfile(userId: string) {\n return Effect.gen(function* () {\n const repository = yield* UserRepositoryService;\n return yield* repository.byId(userId);\n });\n}\n```\n\nThe resulting program carries its success value, its typed failures and its\nrequirements. A Craft component only needs `loadUserProfile`; it does not need\nto know which Layer implements `UserRepositoryService`.\n\n## Choose the right adapter\n\n### `queryEffect`: Effect-backed reads\n\n```typescript\nconst users =\n yield *\n queryEffect('users', {\n params: () => ({ filter: search() }),\n loader: ({ params }) => listUsers(params),\n });\n```\n\nUse it when the result is server or domain state. Craft owns `status`, loading,\nprevious value, cancellation and reloading. The loader returns\n`Effect<Value, Error, Requirements>`.\n\nThe `params` factory and `method` are synchronous. They may read Craft\ndependencies, but must not create an Effect or read an Effect service. The\nloader is the only Effect-aware callback:\n\n```typescript\nconst users =\n yield *\n queryEffect('users', {\n params: function* () {\n const input = yield* searchInput();\n return resolveSearchParams(input);\n },\n loader: ({ params }) => listUsers(params),\n });\n```\n\nThe Effect ESLint rule enforces this boundary. A **declared-synchronous** Effect\nis allowed here through `syncEffect(...)`; for an input that has to suspend, use\na `queryEffect` and feed its settled value to this one.\n\n### `mutationEffect`: Effect-backed writes\n\n```typescript\nconst saveUser =\n yield *\n mutationEffect('saveUser', {\n method: (input: UserInput) => input,\n loader: ({ params }) => saveUserEffect(params),\n });\n```\n\nTrigger it with `yield* saveUser.mutate(input)`. Use the normal Craft\n`insertReactOnMutation` insertion to reload a query or apply an optimistic patch.\nThe mutation `method` only maps its arguments to synchronous params. The\n`loader` is the only Effect-aware callback.\n\n### `asyncProcessEffect`: explicit commands\n\n```typescript\nconst exportUsers =\n yield *\n asyncProcessEffect('exportUsers', {\n method: (filter: Filter) => filter,\n loader: ({ params }) => exportUsersEffect(params),\n });\n\nyield * exportUsers.method(currentFilter);\n```\n\nThe `asyncProcessEffect` method follows the same rule: it returns plain params;\nthe loader owns the asynchronous Effect program.\n\nUse it for an operation with a lifecycle but without a query cache or mutation\nrelationship.\n\n### `methodEffect`: synchronous callable methods\n\nUse `methodEffect` when a domain operation is a synchronous Effect and should be\nexposed as a callable method rather than as a resource:\n\n```typescript\nconst formatPrice = methodEffect('formatPrice', (cents: number) =>\n Effect.gen(function* () {\n yield* SyncOp;\n return `${(cents / 100).toFixed(2)} €`;\n }),\n);\n\nformatPrice(1499); // '14.99 €'\n```\n\nIt is the Effect-aware convenience form of `craftMethod`. The `SyncOp`\nrequirement is mandatory because the method returns immediately. For an Effect\nthat can suspend, use `asyncProcessEffect`, `mutationEffect`, or\n`queryEffect`.\n\n### `computedEffect`: a derived value, not a resource\n\n`computedEffect` runs a **synchronous** Effect in place and hands back a value.\nIt is the adapter for a derivation — a formatted price, a validity flag — where\n`queryEffect` would wrap the answer in a resource with a loading state nothing\ncan ever be in:\n\n```typescript\nimport { computedEffect } from '@craft-ts/effect';\n\nconst totalLabel = computedEffect('totalLabel', function* () {\n const lines = yield* cartLines();\n return cartTotalLabel(lines); // returns the Effect, never runs it\n});\n```\n\nThe factory reads Craft dependencies with `yield*` and **returns** an Effect;\nthe adapter runs it in place against the nearest `provideLayer(...)`. Read the\nresult like any `craftComputed` — no `value`, no `isLoading`, no\n`pendingNode`.\n\nThe Effect it returns must be declared synchronous — `Effect<A, E, SyncOp>` —\nfor the same reason `syncEffect` requires it: a computation is asked for its\nvalue now and cannot suspend to produce it, so one whose `R` does not carry\n`SyncOp` is refused at the call site. See [Run a synchronous member from a\ncomputed](#run-a-synchronous-member-from-a-computed) for what `SyncOp` is and\nthe three mechanisms that check the claim.\n\nUse `syncEffect` instead when the synchronous Effect is not the whole\nderivation — inside a `craftMethod`, a `params`, or a `state` updater.\n\n### `runEffect`: the low-level form\n\nUse `runEffect` when an Effect is yielded directly by a guard, resolver or Craft\nprogram and you need its typed errors to be visible to Craft:\n\n```typescript\nimport { runEffect } from '@craft-ts/effect';\n\nconst user = yield * runEffect(loadUserProfile(userId));\n```\n\nThe adapter is the right choice for most component resources. A bare\n`yield* someEffect` may execute at runtime, but it does not advertise the\nEffect's `E` channel to Craft's route-exception analysis. `runEffect` does.\n\n## Provide services with `Layer`\n\n`provideLayer` attaches a built Effect context to a Craft injector:\n\n```typescript\nexport const appConfig = craftAppConfig({\n providers: [provideLayer(Layer.mergeAll(UserRepositoryLive, SessionLive))],\n});\n```\n\nUse one merged Layer per injector level. A route can add a narrower Layer:\n\n```typescript\nconst routes = craftRoutes('app', [\n {\n path: 'team',\n ...loadCraftComponent(\n () => import('./team'),\n [provideLayer(TeamContextLive)] as const,\n ),\n },\n]);\n```\n\nThe parent context is reused and the child Layer is added for that route. Its\nEffect scope is closed with the route injector.\n\nFor compile-time coverage, compare the program's `Effect.Services<...>` with\nthe values provided by the app and route:\n\n```typescript\ntype Check = EffectRequirementsCheckedDI<\n Effect.Services<typeof loadTeamOverview>,\n AppProvidedEffectServices |\n ProvidedEffectServicesOfRoute<typeof routes._routes, 'team'>\n>;\ntype CanRunCheck = CanRun<Check>;\n```\n\nSee [route-scoped Layers in the Learn path](/learn-effect/06-layers-routing) for\nthe full `AppProvidedDependencyValuesOf` setup.\n\n## Understand the error mapping\n\nThe bridge keeps Effect's distinctions intact:\n\n| Effect outcome | Craft outcome | Handle it with |\n| -------------------------- | ------------------------------------- | ---------------------------------------- |\n| `Effect.succeed(value)` | resource value / generator result | normal rendering |\n| typed `Effect.fail(error)` | Craft exception keyed by `error._tag` | `matchNode`, `catchTag`, route handlers |\n| `Effect.die(defect)` | technical error | error boundary / monitoring |\n| interruption | cancellation | normally no user-facing handler |\n\nUse exhaustive matching for business errors:\n\n```typescript\nmatchNode.exhaustive(resource.exception, '_tag', {\n UserNotFound: () => p('No user was found.'),\n Unauthorized: () => p('Your session has expired.'),\n});\n```\n\nThe error union is only visible to the compiler when the Effect crosses through\n`queryEffect`, `mutationEffect`, `asyncProcessEffect` or `runEffect`.\n\n## Use Effect Schema at data boundaries\n\n`@craft-ts/core` accepts Standard Schema. Effect Schema participates through one\nconversion call:\n\n```typescript\nimport { Schema } from 'effect';\n\nconst UserInput = Schema.toStandardSchemaV1(\n Schema.Struct({\n name: Schema.String,\n email: Schema.String,\n }),\n);\n\nconst saveUser =\n yield *\n mutationEffect('saveUser', {\n methodSchema: UserInput,\n method: (input) => input,\n loader: ({ params }) => saveUserEffect(params),\n });\n```\n\nThis schema interop does not require `@craft-ts/effect`; it follows the Standard\nSchema contract. Use the [schema validation guide](/guide/state/schema-validation#effect-schema)\nfor async decoding and loader result validation.\n\n## Select an Effect service from Craft\n\nMost components should consume a domain operation. A Craft service or adapter\nthat really needs an Effect service can select only the members it uses:\n\n```typescript\nconst { byId } =\n yield * effectService(UserRepositoryService, ({ byId }) => ({ byId }));\n```\n\nThe selection narrows the graph and keeps generic member signatures intact. It\ndoes not replace `Layer`; the service still comes from the nearest\n`provideLayer(...)`.\n\n## Run a synchronous member from a computed\n\n`params`, `craftComputed(...)` and `craftMethod(...)` run on Craft's synchronous\ndriver: they complete on one tick and cannot wait. `Effect<A, E, R>` does not say\nwhether running an Effect will suspend, and a service member makes it worse — a\n`Layer` closes over its dependencies at construction, so a network call and a\npure calculation both surface as `R = never`.\n\nDeclare the difference in `R`, the one channel Effect accumulates:\n\n```typescript\nexport type CartPricingShape = {\n readonly fetchCatalog: (skus: readonly string[]) => Effect.Effect<Catalog>;\n readonly lineTotal: (line: CartLine) => Effect.Effect<number, never, SyncOp>;\n};\n```\n\n`SyncOp` is a phantom requirement: nothing provides it, it costs nothing at\nruntime, and `Effect<A, E, never>` is assignable to `Effect<A, E, SyncOp>` — so\ndeclaring it in the shape is enough, the implementation needs no ceremony. Where\n`R` is inferred (a standalone `Effect.gen` calling nothing already marked), add\n`yield* SyncOp` to the body.\n\nRun it with `syncEffect(...)`, which resolves in place instead of suspending:\n\n```typescript\nconst totalLabel = craftComputed('totalLabel', function* () {\n const cents = yield* syncEffect(cartTotal(yield* lines()));\n return yield* syncEffect(formatPrice(cents));\n});\n```\n\nRequirements other than `SyncOp` travel through untouched — the level in force\nsatisfies them exactly as it does for a loader. The only thing checked at the\ntype level is that `SyncOp` is among them.\n\nThe declaration is a claim, and three independent mechanisms check it: the type\nrefuses an undeclared Effect at the call; `craft-ts/sync-effect-body` reads the\nbody, every branch at once, and rejects one that yields something async; and at\nruntime `syncEffect` goes through `Effect.runSyncExitWith`, which cannot suspend\n— a broken promise throws `CraftEffectNotSynchronous` at the first call rather\nthan freezing the UI.\n\nFull walkthrough: [Declare a synchronous member](/learn-effect/03-effect-domain#declare-a-synchronous-member).\n\n## Testing\n\nUse `mockEffectService` for a focused Layer:\n\n```typescript\nconst repository = mockEffectService(UserRepositoryService, {\n byId: () => Effect.succeed(expectedUser),\n});\n```\n\nCombine it with Craft's register-based tests. The Effect mock covers the Effect\nservice; the Craft register covers every Craft dependency and boundary. An\nunstubbed member fails with `UnstubbedEffectMember` instead of silently returning\nan incomplete value.\n\nSee [testing with Effect](/learn-effect/08-testing) and [browser\nboundaries](/guide/testing/browser-boundaries).\n\n## Package map\n\n| Package | Responsibility |\n| --------------------- | ---------------------------------------------------------------------------------------------------------------- |\n| `@craft-ts/component` | functional Craft components and typed templates |\n| `@craft-ts/core` | Craft primitives, services, routing, forms, testing and the current server-function registry |\n| `@craft-ts/effect` | Effect bridge, `Layer` providers, Effect-aware primitives, service selection, mocks and server execution helpers |\n| `@craft-ts/i18n-effect` | the Effect adapter over an `@craft-ts/i18n` runtime: `provideI18nRuntime`, `translateEffect`, `I18nEffectService` — see [i18n with Effect](/guide/i18n/effect) |\n| `effect` | `Effect`, `Context.Service`, `Layer`, `Schema`, tagged errors and the Effect runtime |\n| `@effect/platform-*` | Effect-native platform adapters; used by the current server-function experiment |\n| `@craft-ts/dev-tools` | generators, migration tools, graph and architecture checks |\n\nInstall only the packages needed by the layer you are building. For example,\nEffect Schema validation can be used with `@craft-ts/core` alone; the bridge and\nEffect-aware resource adapters require `@craft-ts/effect`.\n\n## Server functions: current POC\n\nThe current server-function integration is a **proof of concept**, not a final\nAPI. It currently combines:\n\n- `serverFunction` and `createServerFunctionClient` from `@craft-ts/core`;\n- `executeEffect` and `effectServerMiddleware` from `@craft-ts/effect`;\n- `Effect`/`Layer` on the server;\n- a local HTTP transport and `@effect/platform-node` in the demo.\n\nThe client must import only the server function's type, while the server owns\nthe implementation and server-only Layers. Authentication and authorization\nmust be checked again on the server; a client Layer is never a security boundary.\n\nSee the [server functions POC chapter](/learn-effect/09-server-functions) and\nthe [running demo](https://github.com/craft-ts/craft-ts/tree/main/apps/demo-with-server-function).\nExpect the transport, file conventions, middleware API and production\nintegration to change before this becomes a stable feature.\n\n## Common mistakes\n\n- **Putting every value in Effect:** keep local UI state in `state` and URL state\n in `queryParams`.\n- **Subscribing in a component:** return an Effect from a resource adapter and\n let Craft own loading and cancellation.\n- **Using `Effect.die` for a business case:** use a tagged error in `E` so the UI\n can handle it exhaustively.\n- **Providing a Layer inside a loader:** provide it at app or route scope so its\n lifetime and requirements are visible.\n- **Trusting client context in a server function:** treat it as a claim and\n verify it on the server.\n- **Using a bare `yield* effect` in a route program:** use `runEffect` so Craft\n sees the typed exception union.\n- **Declaring a member `SyncOp` to get it into a computed:** the marker states a\n fact, it does not create one. If the member can suspend, move the work to a\n loader — the runtime will refuse it anyway.\n\n## See also\n\n- [Learn CraftTS with Effect](/learn-effect/)\n- [Which primitive should I use?](/guide/concepts/choose-primitive)\n- [Exceptions as values](/guide/concepts/exceptions)\n- [Program operators](/guide/advanced/program-operators)\n- [Effect Schema](/guide/state/schema-validation#effect-schema)\n- [Effect integration tests](/learn-effect/08-testing)\n"
15
+ "body": "# Using Effect with CraftTS\n\nEffect belongs in CraftTS when the problem is a **domain program**: composing\nservices, modelling typed failures, controlling resources, or running an\noperation that crosses an I/O boundary. CraftTS remains responsible for\ncomponents, fine-grained rendering, reactive state and resource lifecycles.\n\nThe integration is deliberately a boundary, not a second UI runtime:\n\n```text\nCraft component / template\n ↓\nCraft primitive or generator\n ↓\nEffect<A, E, R>\n ↓\nLayer<R> provided by the Craft injector\n```\n\nIf a value is only local UI state, keep it in Craft. If it is a domain operation\nwith typed errors or services, define it as an Effect and adapt it at the Craft\nboundary.\n\n## The short decision\n\n| Need | Use | Why |\n| -------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------- |\n| Toggle, draft, selection or other local UI value | `state` | Craft owns reactive UI state |\n| Read data with an Effect loader | `queryEffect` | loading, caching, cancellation and exceptions are Craft concerns |\n| Derive a reactive value from a synchronous Effect | `computedEffect` | runs a `SyncOp` Effect in place — a value, not a resource |\n| Expose a synchronous Effect as a callable method | `methodEffect` | the Effect counterpart of `craftMethod` |\n| Run a synchronous Effect in a lower-level position | `syncEffect` | `params`, a `craftMethod`, a `state` updater |\n| Write data with an Effect loader | `mutationEffect` | explicit writes and mutation reactions |\n| Run an explicit command | `asyncProcessEffect` | export, refresh, share action or other non-resource process |\n| Provide Effect services | `provideLayer` | app and route injectors own Layer scope |\n| Select a service from a Craft factory | `effectService` | records the Effect service dependency and selected members |\n| Yield one Effect in a Craft generator | `runEffect` | low-level bridge with typed Craft exceptions |\n| Validate data with Effect Schema | `Schema.toStandardSchemaV1(...)` | uses Craft's schema boundary without coupling core to Effect |\n\nThere is intentionally no `stateEffect`. A reactive value is not made better by\nbeing an Effect. Use `state` for the value, and use Effect for the computation\nthat loads or changes it.\n\n## Install the packages\n\n```shell\nnpm i @craft-ts/core@beta @craft-ts/component@beta @craft-ts/effect@beta\nnpm i effect@rc\n```\n\nKeep the three Craft packages on the same version. `@craft-ts/effect` declares\n`effect` as a peer dependency.\n\n## Run Effect diagnostics\n\nFor an Effect-enabled project, make the diagnostics command part of the normal\nfeedback loop:\n\n```shell\nnpm run effect-check\n# or, from the repository root:\nnode tools/run-effect-tsgo.mjs diagnostics \\\n --project apps/demo-effect/tsconfig.json \\\n --severity error,warning,message\n```\n\nThe repository wrapper prints the resolved project scope, the TypeScript\nprogram file count and source candidates excluded by the project configuration,\nthen asks EffectTS to print per-file progress. It refuses a zero-file program,\nwhich catches a wrong `--project`, an over-broad `exclude`, or an `include` that\nmisses the intended source tree before the check can look green.\n\nReview warnings in two groups: the existing baseline that is accepted for the\nproject, and warnings introduced by the current change. Keep the command's\nscope explicit in CI and link a diagnostic back to this page when handing the\nresult to an agent. `--project=...` and `-p ...` are both supported.\n\n## Install the bridge once\n\nThe bridge teaches Craft's generator driver how to execute a yielded Effect.\nInstall it at application bootstrap:\n\n```typescript\nimport { provideAppInitializer } from '@craft-ts/core';\nimport { installCraftEffectBridge } from '@craft-ts/effect';\n\nexport const appConfig = craftAppConfig({\n providers: [\n provideAppInitializer(() => {\n installCraftEffectBridge();\n }),\n ],\n});\n```\n\nIn a test, install it in `beforeEach` and call the returned disposer in\n`afterEach`. Do not install a new bridge in every loader or component.\n\n## Keep components in Craft\n\nA Craft component still has a generator factory and a typed template. The\ncomponent should call a domain operation, not resolve its repository or start a\nfiber from a click handler:\n\n```typescript\nimport { button, craftComponent, p } from '@craft-ts/component';\nimport { queryEffect } from '@craft-ts/effect';\nimport { loadUserProfile } from './profile-domain';\n\nexport const Profile = craftComponent(\n 'Profile',\n {},\n function* () {\n const profile = yield* queryEffect('profile', {\n params: () => 'user-ada',\n loader: ({ params }) => loadUserProfile(params),\n });\n\n return { profile };\n },\n ({ profile }) => [\n p(function* () {\n const user = yield* profile.value();\n return user?.name ?? 'Loading…';\n }),\n button(\n 'reload',\n {\n *click() {\n yield* profile.reload();\n },\n },\n 'Reload',\n ),\n ],\n);\n```\n\nThe template consumes Craft readers. It does not subscribe to an Effect, call\n`Effect.runPromise`, or convert a Promise into a signal manually.\n\n## Define the domain in Effect\n\nUse Effect for domain contracts and implementations. Tagged errors are values in\nthe `E` channel:\n\n```typescript\nimport { Context, Data, Effect, Layer } from 'effect';\n\nexport class UserNotFound extends Data.TaggedError('UserNotFound')<{\n readonly userId: string;\n}> {}\n\nexport type UserRepository = {\n readonly byId: (userId: string) => Effect.Effect<User, UserNotFound>;\n};\n\nexport class UserRepositoryService extends Context.Service<\n UserRepositoryService,\n UserRepository\n>()('app/UserRepository') {}\n\nexport const UserRepositoryLive = Layer.sync(UserRepositoryService)(() => ({\n byId: (userId) => findUserInDatabase(userId),\n}));\n\nexport function loadUserProfile(userId: string) {\n return Effect.gen(function* () {\n const repository = yield* UserRepositoryService;\n return yield* repository.byId(userId);\n });\n}\n```\n\nThe resulting program carries its success value, its typed failures and its\nrequirements. A Craft component only needs `loadUserProfile`; it does not need\nto know which Layer implements `UserRepositoryService`.\n\n## Choose the right adapter\n\n### `queryEffect`: Effect-backed reads\n\n```typescript\nconst users =\n yield *\n queryEffect('users', {\n params: () => ({ filter: search() }),\n loader: ({ params }) => listUsers(params),\n });\n```\n\nUse it when the result is server or domain state. Craft owns `status`, loading,\nprevious value, cancellation and reloading. The loader returns\n`Effect<Value, Error, Requirements>`.\n\nThe `params` factory and `method` are synchronous. They may read Craft\ndependencies, but must not create an Effect or read an Effect service. The\nloader is the only Effect-aware callback:\n\n```typescript\nconst users =\n yield *\n queryEffect('users', {\n params: function* () {\n const input = yield* searchInput();\n return resolveSearchParams(input);\n },\n loader: ({ params }) => listUsers(params),\n });\n```\n\nThe Effect ESLint rule enforces this boundary. A **declared-synchronous** Effect\nis allowed here through `syncEffect(...)`; for an input that has to suspend, use\na `queryEffect` and feed its settled value to this one.\n\n### `mutationEffect`: Effect-backed writes\n\n```typescript\nconst saveUser =\n yield *\n mutationEffect('saveUser', {\n method: (input: UserInput) => input,\n loader: ({ params }) => saveUserEffect(params),\n });\n```\n\nTrigger it with `yield* saveUser.mutate(input)`. Use the normal Craft\n`insertReactOnMutation` insertion to reload a query or apply an optimistic patch.\nThe mutation `method` only maps its arguments to synchronous params. The\n`loader` is the only Effect-aware callback.\n\n### `asyncProcessEffect`: explicit commands\n\n```typescript\nconst exportUsers =\n yield *\n asyncProcessEffect('exportUsers', {\n method: (filter: Filter) => filter,\n loader: ({ params }) => exportUsersEffect(params),\n });\n\nyield * exportUsers.method(currentFilter);\n```\n\nThe `asyncProcessEffect` method follows the same rule: it returns plain params;\nthe loader owns the asynchronous Effect program.\n\nUse it for an operation with a lifecycle but without a query cache or mutation\nrelationship.\n\n### `methodEffect`: synchronous callable methods\n\nUse `methodEffect` when a domain operation is a synchronous Effect and should be\nexposed as a callable method rather than as a resource:\n\n```typescript\nconst formatPrice = methodEffect('formatPrice', (cents: number) =>\n Effect.gen(function* () {\n yield* SyncOp;\n return `${(cents / 100).toFixed(2)} €`;\n }),\n);\n\nformatPrice(1499); // '14.99 €'\n```\n\nIt is the Effect-aware convenience form of `craftMethod`. The `SyncOp`\nrequirement is mandatory because the method returns immediately. For an Effect\nthat can suspend, use `asyncProcessEffect`, `mutationEffect`, or\n`queryEffect`.\n\n### `computedEffect`: a derived value, not a resource\n\n`computedEffect` runs a **synchronous** Effect in place and hands back a value.\nIt is the adapter for a derivation — a formatted price, a validity flag — where\n`queryEffect` would wrap the answer in a resource with a loading state nothing\ncan ever be in:\n\n```typescript\nimport { computedEffect } from '@craft-ts/effect';\n\nconst totalLabel = computedEffect('totalLabel', function* () {\n const lines = yield* cartLines();\n return cartTotalLabel(lines); // returns the Effect, never runs it\n});\n```\n\nThe factory reads Craft dependencies with `yield*` and **returns** an Effect;\nthe adapter runs it in place against the nearest `provideLayer(...)`. Read the\nresult like any `craftComputed` — no `value`, no `isLoading`, no\n`pendingNode`.\n\nThe Effect it returns must be declared synchronous — `Effect<A, E, SyncOp>` —\nfor the same reason `syncEffect` requires it: a computation is asked for its\nvalue now and cannot suspend to produce it, so one whose `R` does not carry\n`SyncOp` is refused at the call site. See [Run a synchronous member from a\ncomputed](#run-a-synchronous-member-from-a-computed) for what `SyncOp` is and\nthe three mechanisms that check the claim.\n\nUse `syncEffect` instead when the synchronous Effect is not the whole\nderivation — inside a `craftMethod`, a `params`, or a `state` updater.\n\n### `runEffect`: the low-level form\n\nUse `runEffect` when an Effect is yielded directly by a guard, resolver or Craft\nprogram and you need its typed errors to be visible to Craft:\n\n```typescript\nimport { runEffect } from '@craft-ts/effect';\n\nconst user = yield * runEffect(loadUserProfile(userId));\n```\n\nThe adapter is the right choice for most component resources. A bare\n`yield* someEffect` may execute at runtime, but it does not advertise the\nEffect's `E` channel to Craft's route-exception analysis. `runEffect` does.\n\n## Provide services with `Layer`\n\n`provideLayer` attaches a built Effect context to a Craft injector:\n\n```typescript\nexport const appConfig = craftAppConfig({\n providers: [provideLayer(Layer.mergeAll(UserRepositoryLive, SessionLive))],\n});\n```\n\nUse one merged Layer per injector level. A route can add a narrower Layer:\n\n```typescript\nconst routes = craftRoutes('app', [\n {\n path: 'team',\n ...loadCraftComponent(() => import('./team'), [\n provideLayer(TeamContextLive),\n ] as const),\n },\n]);\n```\n\nThe parent context is reused and the child Layer is added for that route. Its\nEffect scope is closed with the route injector.\n\nFor compile-time coverage, compare the program's `Effect.Services<...>` with\nthe values provided by the app and route:\n\n```typescript\ntype Check = EffectRequirementsCheckedDI<\n Effect.Services<typeof loadTeamOverview>,\n | AppProvidedEffectServices\n | ProvidedEffectServicesOfRoute<typeof routes._routes, 'team'>\n>;\ntype CanRunCheck = CanRun<Check>;\n```\n\nSee [route-scoped Layers in the Learn path](/learn-effect/06-layers-routing) for\nthe full `AppProvidedDependencyValuesOf` setup.\n\n## Understand the error mapping\n\nThe bridge keeps Effect's distinctions intact:\n\n| Effect outcome | Craft outcome | Handle it with |\n| -------------------------- | ------------------------------------- | --------------------------------------- |\n| `Effect.succeed(value)` | resource value / generator result | normal rendering |\n| typed `Effect.fail(error)` | Craft exception keyed by `error._tag` | `matchNode`, `catchTag`, route handlers |\n| `Effect.die(defect)` | technical error | error boundary / monitoring |\n| interruption | cancellation | normally no user-facing handler |\n\nUse exhaustive matching for business errors:\n\n```typescript\nmatchNode.exhaustive(resource.exception, '_tag', {\n UserNotFound: () => p('No user was found.'),\n Unauthorized: () => p('Your session has expired.'),\n});\n```\n\nThe error union is only visible to the compiler when the Effect crosses through\n`queryEffect`, `mutationEffect`, `asyncProcessEffect` or `runEffect`.\n\n## Use Effect Schema at data boundaries\n\n`@craft-ts/core` accepts Standard Schema. Effect Schema participates through one\nconversion call:\n\n```typescript\nimport { Schema } from 'effect';\n\nconst UserInput = Schema.toStandardSchemaV1(\n Schema.Struct({\n name: Schema.String,\n email: Schema.String,\n }),\n);\n\nconst saveUser =\n yield *\n mutationEffect('saveUser', {\n methodSchema: UserInput,\n method: (input) => input,\n loader: ({ params }) => saveUserEffect(params),\n });\n```\n\nThis schema interop does not require `@craft-ts/effect`; it follows the Standard\nSchema contract. Use the [schema validation guide](/guide/state/schema-validation#effect-schema)\nfor async decoding and loader result validation.\n\n## Select an Effect service from Craft\n\nMost components should consume a domain operation. A Craft service or adapter\nthat really needs an Effect service can select only the members it uses:\n\n```typescript\nconst { byId } =\n yield * effectService(UserRepositoryService, ({ byId }) => ({ byId }));\n```\n\nThe selection narrows the graph and keeps generic member signatures intact. It\ndoes not replace `Layer`; the service still comes from the nearest\n`provideLayer(...)`.\n\n## Run a synchronous member from a computed\n\n`params`, `craftComputed(...)` and `craftMethod(...)` run on Craft's synchronous\ndriver: they complete on one tick and cannot wait. `Effect<A, E, R>` does not say\nwhether running an Effect will suspend, and a service member makes it worse — a\n`Layer` closes over its dependencies at construction, so a network call and a\npure calculation both surface as `R = never`.\n\nDeclare the difference in `R`, the one channel Effect accumulates:\n\n```typescript\nexport type CartPricingShape = {\n readonly fetchCatalog: (skus: readonly string[]) => Effect.Effect<Catalog>;\n readonly lineTotal: (line: CartLine) => Effect.Effect<number, never, SyncOp>;\n};\n```\n\n`SyncOp` is a phantom requirement: nothing provides it, it costs nothing at\nruntime, and `Effect<A, E, never>` is assignable to `Effect<A, E, SyncOp>` — so\ndeclaring it in the shape is enough, the implementation needs no ceremony. Where\n`R` is inferred (a standalone `Effect.gen` calling nothing already marked), add\n`yield* SyncOp` to the body.\n\nRun it with `syncEffect(...)`, which resolves in place instead of suspending:\n\n```typescript\nconst totalLabel = craftComputed('totalLabel', function* () {\n const cents = yield* syncEffect(cartTotal(yield* lines()));\n return yield* syncEffect(formatPrice(cents));\n});\n```\n\nRequirements other than `SyncOp` travel through untouched — the level in force\nsatisfies them exactly as it does for a loader. The only thing checked at the\ntype level is that `SyncOp` is among them.\n\nThe declaration is a claim, and three independent mechanisms check it: the type\nrefuses an undeclared Effect at the call; `craft-ts/sync-effect-body` reads the\nbody, every branch at once, and rejects one that yields something async; and at\nruntime `syncEffect` goes through `Effect.runSyncExitWith`, which cannot suspend\n— a broken promise throws `CraftEffectNotSynchronous` at the first call rather\nthan freezing the UI.\n\nFull walkthrough: [Declare a synchronous member](/learn-effect/03-effect-domain#declare-a-synchronous-member).\n\n## Testing\n\nUse `mockEffectService` for a focused Layer:\n\n```typescript\nconst repository = mockEffectService(UserRepositoryService, {\n byId: () => Effect.succeed(expectedUser),\n});\n```\n\nCombine it with Craft's register-based tests. The Effect mock covers the Effect\nservice; the Craft register covers every Craft dependency and boundary. An\nunstubbed member fails with `UnstubbedEffectMember` instead of silently returning\nan incomplete value.\n\nSee [testing with Effect](/learn-effect/08-testing) and [browser\nboundaries](/guide/testing/browser-boundaries).\n\n## Package map\n\n| Package | Responsibility |\n| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `@craft-ts/component` | functional Craft components and typed templates |\n| `@craft-ts/core` | Craft primitives, services, routing, forms, testing and the current server-function registry |\n| `@craft-ts/effect` | Effect bridge, `Layer` providers, Effect-aware primitives, service selection, mocks and server execution helpers |\n| `@craft-ts/i18n-effect` | the Effect adapter over an `@craft-ts/i18n` runtime: `provideI18nRuntime`, `translateEffect`, `I18nEffectService` — see [i18n with Effect](/guide/i18n/effect) |\n| `effect` | `Effect`, `Context.Service`, `Layer`, `Schema`, tagged errors and the Effect runtime |\n| `@effect/platform-*` | Effect-native platform adapters; used by the current server-function experiment |\n| `@craft-ts/dev-tools` | generators, migration tools, graph and architecture checks |\n\nInstall only the packages needed by the layer you are building. For example,\nEffect Schema validation can be used with `@craft-ts/core` alone; the bridge and\nEffect-aware resource adapters require `@craft-ts/effect`.\n\n## Server functions: current POC\n\nThe current server-function integration is a **proof of concept**, not a final\nAPI. It currently combines:\n\n- `serverFunction` and `createServerFunctionClient` from `@craft-ts/core`;\n- `executeEffect` and `effectServerMiddleware` from `@craft-ts/effect`;\n- `Effect`/`Layer` on the server;\n- a local HTTP transport and `@effect/platform-node` in the demo.\n\nThe client must import only the server function's type, while the server owns\nthe implementation and server-only Layers. Authentication and authorization\nmust be checked again on the server; a client Layer is never a security boundary.\n\nSee the [server functions POC chapter](/learn-effect/09-server-functions) and\nthe [running demo](https://github.com/craft-ts/craft-ts/tree/main/apps/demo-with-server-function).\nExpect the transport, file conventions, middleware API and production\nintegration to change before this becomes a stable feature.\n\n## Common mistakes\n\n- **Putting every value in Effect:** keep local UI state in `state` and URL state\n in `queryParams`.\n- **Subscribing in a component:** return an Effect from a resource adapter and\n let Craft own loading and cancellation.\n- **Using `Effect.die` for a business case:** use a tagged error in `E` so the UI\n can handle it exhaustively.\n- **Providing a Layer inside a loader:** provide it at app or route scope so its\n lifetime and requirements are visible.\n- **Trusting client context in a server function:** treat it as a claim and\n verify it on the server.\n- **Using a bare `yield* effect` in a route program:** use `runEffect` so Craft\n sees the typed exception union.\n- **Declaring a member `SyncOp` to get it into a computed:** the marker states a\n fact, it does not create one. If the member can suspend, move the work to a\n loader — the runtime will refuse it anyway.\n\n## See also\n\n- [Learn CraftTS with Effect](/learn-effect/)\n- [Which primitive should I use?](/guide/concepts/choose-primitive)\n- [Exceptions as values](/guide/concepts/exceptions)\n- [Program operators](/guide/advanced/program-operators)\n- [Effect Schema](/guide/state/schema-validation#effect-schema)\n- [Effect integration tests](/learn-effect/08-testing)\n"
16
16
  },
17
17
  {
18
18
  "path": "/guide/advanced/observability",
@@ -74,6 +74,11 @@
74
74
  "title": "craftRegisterFor",
75
75
  "body": "# craftRegisterFor\n\n`craftRegisterFor` exposes, within a Craft injection scope, the services,\ncomponents and directives that are **currently alive** in it.\n\n**Use it when** a parent must drive several children without each child having to\npush a bespoke API upwards: counters, audio players, selected items, validation\nacross a form section.\n**Not when** one known child is involved — pass it a service or an input\ninstead. A registry trades explicitness for reach.\n\n## Declaring a registry\n\nThe registry is typed from the Craft targets it accepts:\n\n```ts\nimport { craftComputed, craftRegisterFor } from '@craft-ts/core';\n\nconst { RegisterForCounter, provideRegisterForCounter } = craftRegisterFor(\n 'Counter',\n [Counter, CounterChild],\n);\n```\n\nThe first argument is the registry's mandatory name. It generates the two public\nhelpers `RegisterForCounter` and `provideRegisterForCounter`, a convention that\nlets several registries coexist in one scope without name collisions.\n\nWith a single target, the array can be omitted:\n\n```ts\nconst { RegisterForCounter } = craftRegisterFor(\n 'Counter',\n Counter,\n ({ Counter }) => ({\n total: craftComputed('total', function* () {\n return (yield* Counter())?.length ?? 0;\n }),\n }),\n);\n\nconst counters = yield* RegisterForCounter();\nconst total = craftComputed('total', function* () {\n return (yield* counters())?.length ?? 0;\n});\n```\n\nIf a projection uses several groups, every target must be declared:\n\n```ts\ncraftRegisterFor(\n 'Counter',\n [Counter, CounterChild],\n ({ Counter, CounterChild }) => ({\n total: craftComputed('total', function* () {\n return (yield* Counter())?.length ?? 0;\n }),\n incrementAll: function* () {\n for (const { ref } of (yield* CounterChild()) ?? []) {\n yield* ref.increment();\n }\n },\n }),\n);\n```\n\nThen add the providers returned by `provideRegisterForCounter()` to the scope\nthat should observe the instances:\n\n```ts\nexport const RegisterForDemo = craftComponent({\n name: 'RegisterForDemo',\n providers: [provideRegisterForCounter()],\n // ...\n});\n```\n\nBy default the registry also includes `global` services resolved under that\nscope. To restrict observation to services whose scope matches the parent:\n\n```ts\ncraftRegisterFor('Counter', [Counter], { includeGlobal: false });\n```\n\nThe first declared target is reachable through `RegisterForCounter()` directly;\nadditional targets get their own property, e.g.\n`RegisterForCounter.CounterChild()`.\n\n## The common case — driving child components\n\nEach child creates a `toProvide` service, and the parent providing the registry\nobserves them:\n\n```ts\nconst { Counter, provideCounter } = craftService(\n { name: 'Counter', providedIn: 'toProvide' },\n function* () {\n const counter = yield* state(\n 'counter',\n 0,\n ({ update }) => ({\n increment: () => update((value) => value + 1),\n decrement: () => update((value) => value - 1),\n }),\n );\n\n return counter;\n },\n);\n\nconst CounterChild = craftComponent(\n 'CounterChild',\n { providers: [provideCounter()] },\n function* () {\n return yield* Counter();\n },\n ({ counter }) => div(counter),\n);\n\nconst { RegisterForCounter, provideRegisterForCounter } = craftRegisterFor(\n 'Counter',\n [Counter, CounterChild],\n);\n\nconst CounterBoard = craftComponent(\n 'CounterBoard',\n { providers: [provideRegisterForCounter()] },\n function* () {\n const counters = yield* RegisterForCounter();\n const children = yield* RegisterForCounter.CounterChild();\n\n return {\n incrementAll: function* () {\n for (const { ref } of (yield* counters()) ?? []) {\n yield* ref.increment();\n }\n },\n childCount: craftComputed('childCount', function* () {\n return (yield* children())?.length ?? 0;\n }),\n };\n },\n ({ incrementAll, childCount }) =>\n section([\n button({ click: incrementAll }, 'Increment every child'),\n p(function* () {\n return `Active children: ${yield* childCount()}`;\n }),\n forNode([1, 2, 3], () => CounterChild({})),\n ]),\n);\n```\n\nWhen a child is added, its `Counter` appears in the group. When it leaves the\nDOM, the group updates on its own.\n\n## Reading a group\n\nGroups are yieldable from a Craft factory. Their signal is `undefined` while no\ninstance is registered, and returns to `undefined` when the last one is\ndestroyed:\n\n```ts\nconst counters = yield* RegisterForCounter();\n\nconst incrementAll = function* () {\n for (const { ref } of (yield* counters()) ?? []) {\n yield* ref.increment();\n }\n};\n```\n\nEach entry carries:\n\n- `ref` — the value produced by the service, or the context returned by the\n component/directive factory;\n- `hostName` — the name of the host scope that created the entry.\n\nThe signal is live: the parent never re-subscribes when a child appears or\ndisappears.\n\n## Partial exposure\n\nAs with `craftService`, a group can expose only the façade the parent needs. The\nfirst argument stays `undefined` to keep the yieldable-helper syntax, and\n`$self` is the group's full signal:\n\n```ts\nconst childComponents = yield* RegisterForCounter.CounterChild(\n undefined,\n ({ $self }) => ({\n total: craftComputed(function* () {\n return (yield* $self())?.length ?? 0;\n }),\n incrementAll: function* () {\n for (const { ref } of (yield* $self()) ?? []) {\n yield* ref.increment();\n }\n },\n decrementAll: function* () {\n for (const { ref } of (yield* $self()) ?? []) {\n yield* ref.decrement();\n }\n },\n }),\n);\n```\n\nThe parent then keeps only `total`, `incrementAll` and `decrementAll`. The\ndependency stays precise — the computed values read the group's signal, and\ninstances are still added and removed automatically.\n\n## Derived registry properties\n\nTo share common projections, the second parameter of `craftRegisterFor` receives\nthe groups' signals directly:\n\n```ts\nconst { RegisterForCounter, provideRegisterForCounter } = craftRegisterFor(\n 'Counter',\n [Counter, CounterChild],\n ({ Counter, CounterChild }) => ({\n totalCounter: craftComputed('totalCounter', function* () {\n return (yield* Counter())?.length ?? 0;\n }),\n incrementAllCounterChild: function* () {\n for (const { ref } of (yield* CounterChild()) ?? []) {\n yield* ref.increment();\n }\n },\n decrementAllCounterChild: function* () {\n for (const { ref } of (yield* CounterChild()) ?? []) {\n yield* ref.decrement();\n }\n },\n }),\n);\n```\n\nEach derived property becomes a yieldable helper:\n\n```ts\nconst totalCounter = yield* RegisterForCounter.totalCounter();\nconst incrementAll = yield* RegisterForCounter.incrementAllCounterChild();\n\nconsole.log(yield* totalCounter());\nyield* incrementAll();\n```\n\nFor a single-target registry, the main call also returns the signal enriched\nwith those derived properties — so the value stays callable for the raw entries\nwhile exposing `total` and the added methods:\n\n```ts\nconst childComponents = yield* RegisterForCounterChild();\n\nconst entries = yield* childComponents();\nconst total = yield* childComponents.total();\nyield* childComponents.incrementAllChildCounter();\nyield* childComponents.decrementAllChildCounter();\n```\n\nIn a Craft template, pass a method straight to an event and call signals inside\na reactive callback:\n\n```ts\nbutton({ click: childComponents.incrementAllChildCounter }, 'Increment all');\nspan(function* () {\n return `Children: ${yield* childComponents.total()}`;\n});\n```\n\nThe main group, the additional groups and the derived properties can all be used\ntogether. Derived properties are computed once per registry injector and keep\nthe reactive signals the groups provide.\n\n## Registering a directive\n\nCraft directives can be targets too:\n\n```ts\nconst { RegisterForCounter } = craftRegisterFor('Counter', [\n CounterChild,\n CounterDebugDirective,\n]);\n\nconst debugEntries = yield* RegisterForCounter.CounterDebugDirective();\ndebugEntries()?.forEach(({ hostName, ref }) => {\n console.debug('directive active', hostName, ref);\n});\n```\n\nA functional directive has no class instance, so `ref` is the factory context of\nthe decorated component. Its `hostName` remains specific to the directive and\nits instance, which is what lets you tell several identical directives apart on\nthe same screen.\n\n## Lifecycle and references\n\nServices are registered when their yield resolves. The runtime attaches their\nremoval to the destruction of the injector that carries them.\n\nCraft components and directives are functional factories with no class instance,\nso `ref` is their factory context. For a directive used with `.pipe(...)`, the\nfinal component's context is exposed, because that is the execution scope the\ndirective shares.\n\nEvery Craft component automatically gets a host tag of the form\n`component:<ComponentName>#<id>`, so `provideHostName` is not needed in a\ncomponent's providers — it stays useful only to override that automatic name.\nDirectives applied to an element get their own `hostName`, generated from the\ndirective name and an instance id. These names distinguish two identical\ninstances and are usable for diagnostics and observability.\n\nEntries are removed automatically in every case: destruction of the\ncomponent/directive, destruction of its DI scope, or replacement of a\ncomposition.\n\n## Pitfalls\n\n::: warning An empty registry is not an error\nCompilation checks that the target you pass to `craftRegisterFor` is a valid\nCraft service, component or directive — but it cannot check that an instance\nwill ever be created. If no registered target exists in the executed code, there\nis no compile error and no runtime error: the signal is simply `undefined`.\n:::\n\n::: warning Craft targets only\n`craftRegisterFor` does not detect arbitrary classes. It targets\n`craftService`, `craftComponent` and `craftDirective`, whose scope and lifecycle\nthe runtime knows.\n:::\n\n**Declaring the same target twice** in the list is not supported — each target\nappears once.\n\n**Treating the group signal as always populated.** It is `undefined` before the\nfirst instance and after the last one; the `?.` is not optional.\n\n::: details Extending the mechanism — target and yield wrappers\nThe registry rests on two separate pieces:\n\n1. a **yield wrapper** observes services as they are actually resolved;\n2. the component/directive **runtime** reports their creation and ties cleanup\n to their lifecycle.\n\nThe first is `provideCraftTargetWrapper`, documented on\n[Target wrapper](/guide/app/target-wrapper). The second is\n`provideServiceYieldWrapper`, the low-level hook `craftRegisterFor` uses to wrap\nevery Craft service resolution in the scope where the yield runs — deliberately\nclose to `provideFnWrapper`, but limited to service yields:\n\n```ts\nimport {\n provideServiceYieldWrapper,\n type ServiceYieldContext,\n} from '@craft-ts/core';\n\nfunction* reportServiceYield(\n context: ServiceYieldContext,\n next: () => Generator<unknown, unknown, unknown>,\n) {\n const startedAt = performance.now();\n const value = yield* next();\n\n console.debug('service resolved', {\n name: context.name,\n hostScope: context.hostScope,\n duration: performance.now() - startedAt,\n });\n\n return value;\n}\n\nexport const providers = [\n provideServiceYieldWrapper(\n 'Warning: the wrapper runs in the current Craft injection context.',\n reportServiceYield,\n ),\n];\n```\n\n`context.resolve()` resolves the real service; `next()` keeps the wrapper chain\nintact. Wrappers compose in registration order — the first is the outermost. The\ncontext provides `name`, `scope`, `hostScope`, `injector` and `resolve`. Like\n`provideFnWrapper`, this hook suits cross-cutting concerns — registries,\nmetrics, traces, diagnostics — not business logic.\n\nA new tool can reuse `provideServiceYieldWrapper` to observe services without\n`craftRegisterFor` at all. For functional Craft targets the runtime also exposes\nits internal registration primitives, so another specialised view can be built —\nbut `craftRegisterFor` stays the recommended application-level API.\n:::\n\n## See Also\n\n- [Target wrapper](/guide/app/target-wrapper) — the extension point underneath\n- [craftService](/guide/app/craft-service)\n- [Customization](/guide/components/customization)\n"
76
76
  },
77
+ {
78
+ "path": "/guide/app/server-functions",
79
+ "title": "Public and protected server functions",
80
+ "body": "# Public and protected server functions\n\n`serverFunction` is a transport contract. Authentication and authorization are\nmiddleware concerns: do not add an `access: 'admin'` option or a parallel\n`protectedServerFunction` helper.\n\n## The canonical path\n\n```text\nclient middleware\n → handshake / client context\n → existing server middleware\n → verified session and role\n → server-function handler\n → Effect service and Layer\n```\n\nThe browser may announce an identifier through `clientContext`, but that value\nis untrusted. The server must load the session again and compare the claim with\nthe verified identity. A missing, expired or revoked session must stop the\nmiddleware chain before the handler runs.\n\n## Transport failures\n\nEvery client-side server-function transport failure is returned as a typed\n`HttpError`, including a lost connection, an aborted request, an unavailable\n`fetch` implementation or an unreadable response. Network failures use\n`status: 0` and `statusText: 'Unknown Error'`, following the same convention as\n`CraftHttpClient`; the original failure is available in the error payload's\n`body` field.\n\nThis means callers can handle connection loss through the normal Craft\nexception path instead of adding a raw Promise rejection handler:\n\n```ts\nconst result = yield * getAnimals({});\nif (isCraftException(result) && result._tag === 'HttpError') {\n // show a retry action or an offline state\n}\n```\n\nThe same normalization is applied to custom transports registered with\n`provideServerFunctionTransport(...)`. Business failures returned by the\nserver keep their own typed tags and are not converted to `HttpError`.\n\n## Public function\n\n```ts\nexport const listPublicAnimals = serverFunction(\n 'animals.public-list',\n inputSchema,\n { exposure: 'client', output: outputSchema },\n).handler(({ input }) =>\n Effect.gen(function* () {\n const repository = yield* AnimalRepository;\n return yield* repository.list(input.filter);\n }),\n);\n```\n\n## Protected function\n\nReuse the same middleware mechanism for the protected path:\n\n```ts\nexport const listAdminAnimals = serverFunction(\n 'animals.admin-list',\n inputSchema,\n { exposure: 'client', output: outputSchema },\n)\n .use(requireAdminSession)\n .handler(({ input }) =>\n Effect.gen(function* () {\n const repository = yield* AnimalRepository;\n return yield* repository.listForAdmin(input.filter);\n }),\n );\n```\n\n`requireAdminSession` is a `craftMiddleware(...).server(...)` value. It should\nreturn an explicit authentication/authorization failure when there is no valid\nsession or the role is insufficient. The handler is then only responsible for\nthe business operation.\n\nFor middleware shared by many functions, the optional\n`createServerFunctionFactory([middleware])` factory applies those existing\n`.use(...)` calls in order; it does not introduce a new security policy API.\n\nSee the executable public/protected example in\n[`apps/demo-with-server-function`](https://github.com/craft-ts/craft-ts/tree/main/apps/demo-with-server-function),\nespecially `craftMiddleware`, `clientContext`, `craftHandshake` and `.use(...)`.\nIts server tests cover no session, a valid session and revocation. Keep the\nserver test beside the function so the middleware wiring remains visible.\nThe demo maps the two session failures to explicit 401 responses and maps a\nnon-admin session to a 403 response.\n\n## Find the pieces\n\n- [`serverFunction`](/learn-effect/09-server-functions)\n- [`craftMiddleware`](/learn-effect/09-server-functions#middleware-and-security)\n- [`clientContext`](/learn-effect/09-server-functions#middleware-and-security)\n- [`craftHandshake`](/learn-effect/09-server-functions#middleware-and-security)\n- [Effect Layers and requirements](/learn-effect/06-layers-routing)\n\nUseful search terms are `auth`, `authentication`, `authorization`, `session`,\n`role`, `access policy` and `middleware`.\n"
81
+ },
77
82
  {
78
83
  "path": "/guide/app/service-scopes",
79
84
  "title": "Service scopes",
@@ -177,7 +182,7 @@
177
182
  {
178
183
  "path": "/guide/create-project",
179
184
  "title": "Create a CraftTS project",
180
- "body": "# Create a CraftTS project\n\nUse `craft create` to generate a framework-independent CraftTS application\nwith routing, a typed API example, linting, tests, and the architecture\ncontract already wired up.\n\n## Prerequisites\n\nThe beta toolchain requires Node.js 20.19 or newer. The `craft` executable is\npublished by `@craft-ts/dev-tools`; it is not provided by the unrelated npm\npackage named `craft`.\n\nFor a new project, invoke the executable explicitly through `npx`:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app\n```\n\nThe first `--yes` belongs to `npx`: it accepts the temporary package\ninstallation. The command remains interactive because `craft create` itself\nwas not given `--yes`.\n\nThe command uses the published `beta` package. A checkout of CraftTS can\ncontain a newer creation flow than the version currently published on npm;\ncheck the resolved version with `npm view @craft-ts/dev-tools@beta version` if\nthe prompts shown by your terminal do not match this page.\n\n## Interactive creation\n\nRun the command in a real terminal without `craft create --yes`:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app\n```\n\nThe generator presents menus for:\n\n- the frontend runtime: `plain` or `effect`;\n- the backend runtime: `none`, `promise`, or `effect`;\n- type-safe i18n, its locales, and its default locale;\n- the design system;\n- typed CSS;\n- a standalone or Nx workspace;\n- CraftTS and, when Effect is selected, EffectTS source references for agent\n context;\n- integrations for Codex, Cursor, Claude Code, or Gemini CLI.\n\nThe frontend and backend choices are independent. To create a plain browser\napplication whose server functions use Effect v4, choose `plain` for the\nfrontend and `effect` for the backend.\n\nUse `↑`/`↓` to move and `Enter` to confirm a single choice. For locales and\nagent integrations, use `Space` to select or deselect several items, then\n`Enter` to confirm. The project directory remains a text field because it is\na free-form path. If the directory is omitted, the generator asks for it too:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create\n```\n\nThe agent question is a multi-selection list. Use `↑`/`↓` to move, `Space` to\nselect or deselect an integration, and `Enter` to confirm. Codex starts\nselected, preserving the default used by scripted creation. Every starter\nreceives an `AGENTS.md` project guide describing its selected runtimes and\nfeatures; selected integrations additionally receive their editor-specific\nproject instructions and skills.\n\n### Creating inside an existing Git repository\n\nAn existing `.git` directory makes the destination non-empty. Generate into\nthe current repository with `--force`:\n\n```bash\ncd pet-foster-family\nnpx --yes --package @craft-ts/dev-tools@beta craft create . --force\n```\n\n`--force` only permits writing into a non-empty destination; it does not turn\noff the configuration prompts. Review generated file changes before\ncommitting when the repository already contains application code.\n\nDuring the interactive flow, reference sources are cloned by default:\n\n- CraftTS sources go into `.references/craft-ts`;\n- EffectTS sources are also cloned when an Effect frontend or backend is\n selected;\n- the sources are available to agents without replacing the installed npm\n packages.\n\nAnswer `n` to opt out. In non-interactive mode, references remain opt-in so\nthat `--yes` does not silently perform network clones; use\n`--references=craft-ts` or `--references=all` explicitly.\n\nThe cloned repositories are reference material for coding agents only. The\ngenerated application always imports the published CraftTS and EffectTS npm\npackages from `package.json`; it does not use `file:` dependencies or\nTypeScript/Vite aliases to the clones. Use `npm run update:references` to fetch\nthe requested refs and refresh the recorded SHAs.\n\n## Non-interactive creation\n\nPass `--yes` after `create` to use defaults and disable all prompts. Combine it\nwith explicit options when the generated configuration must be reproducible:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --effect=none --agents=codex\n```\n\nFor a minimal plain starter:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --effect=none --i18n=none --design-system=none --no-typed-css \\\n --agents=none\n```\n\nTo create a backend-only Effect project and clone both reference sources:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --frontend-runtime=plain --backend-runtime=effect \\\n --references=all\n```\n\nThe main configuration options are:\n\n| Option | Values | Purpose |\n| --- | --- | --- |\n| `--effect` | `v4`, `none` | Select the Effect v4 or plain starter |\n| `--frontend-runtime` | `plain`, `effect` | Choose the frontend runtime |\n| `--backend-runtime` | `none`, `promise`, `effect` | Choose server functions |\n| `--effect-scope` | `none`, `frontend`, `backend`, `both` | Set Effect placement |\n| `--agents` | comma-separated names or `none` | Add editor-specific agent integrations; `AGENTS.md` is always generated |\n| `--i18n` | `strict`, `loose`, `none` | Configure type-safe i18n |\n| `--design-system` | `basic`, `none` | Include the design-system starter |\n| `--typed-css` | flag / `--no-typed-css` | Enable or disable typed CSS |\n| `--workspace` | `standalone`, `nx` | Choose the workspace layout |\n| `--references` | `none`, `craft-ts`, `all` | Include source references |\n| `--force` | flag | Allow an existing non-empty destination |\n| `--json` | flag | Print the effective configuration as JSON |\n\nUse `craft create --help` to see the complete list:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create --help\n```\n\n## After generation\n\nThe generator creates a Git repository when the destination is not already\ninside another repository. It does not create a commit. The generated\n`.gitignore` excludes `node_modules/`, build outputs, test reports, and local\nreference clones.\n\nInstall dependencies and start the generated application:\n\n```bash\ncd my-app\nnpm install\nnpm run dev\n```\n\nThe generated project also includes the following checks:\n\n```bash\nnpm run lint\nnpm run typecheck\nnpm test\nnpm run architecture\nnpm run build\n```\n\nWith a backend, `src/server/application.ts` owns the registry and runtime\nLayer, while `src/server/node-http.ts` is only the Node stream adapter.\n`server.ts` re-exports both for compatibility. In the backend-only Effect\nprofile, the browser remains plain CraftTS; Effect services, middleware and\nerror projections stay under the server boundary.\n\n## Troubleshooting\n\n### `could not determine executable to run`\n\nIf the error mentions `craft@0.1.0`, `npx` resolved the unrelated public npm\npackage named `craft`. Use the explicit `--package @craft-ts/dev-tools@beta`\nform shown above.\n\nIf `@craft-ts/dev-tools` is already installed in the project, its local binary\ncan also be called with:\n\n```bash\nnpx craft create my-app\n```\n\nThe explicit form is still the safest command when bootstrapping a project\nthat has no `package.json` yet.\n"
185
+ "body": "# Create a CraftTS project\n\nUse `craft create` to generate a framework-independent CraftTS application\nwith routing, a typed API example, linting, tests, and the architecture\ncontract already wired up.\n\n## Prerequisites\n\nThe beta toolchain requires Node.js 20.19 or newer. The `craft` executable is\npublished by `@craft-ts/dev-tools`; it is not provided by the unrelated npm\npackage named `craft`.\n\nFor a new project, invoke the executable explicitly through `npx`:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app\n```\n\nThe first `--yes` belongs to `npx`: it accepts the temporary package\ninstallation. The command remains interactive because `craft create` itself\nwas not given `--yes`.\n\nThe command uses the published `beta` package. A checkout of CraftTS can\ncontain a newer creation flow than the version currently published on npm;\ncheck the resolved version with `npm view @craft-ts/dev-tools@beta version` if\nthe prompts shown by your terminal do not match this page.\n\n## Interactive creation\n\nRun the command in a real terminal without `craft create --yes`:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app\n```\n\nThe generator presents menus for:\n\n- the frontend runtime: `plain` or `effect`;\n- the backend runtime: `none`, `promise`, or `effect`;\n- type-safe i18n, its locales, and its default locale;\n- the design system;\n- typed CSS;\n- a standalone or Nx workspace;\n- CraftTS and, when Effect is selected, EffectTS source references for agent\n context;\n- integrations for Codex, Cursor, Claude Code, or Gemini CLI.\n\nThe frontend and backend choices are independent. To create a plain browser\napplication whose server functions use Effect v4, choose `plain` for the\nfrontend and `effect` for the backend.\n\nUse `↑`/`↓` to move and `Enter` to confirm a single choice. For locales and\nagent integrations, use `Space` to select or deselect several items, then\n`Enter` to confirm. The project directory remains a text field because it is\na free-form path. If the directory is omitted, the generator asks for it too:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create\n```\n\nThe agent question is a multi-selection list. Use `↑`/`↓` to move, `Space` to\nselect or deselect an integration, and `Enter` to confirm. Codex starts\nselected, preserving the default used by scripted creation. Every starter\nreceives an `AGENTS.md` project guide describing its selected runtimes and\nfeatures; selected integrations additionally receive their editor-specific\nproject instructions and skills.\n\n### Creating inside an existing Git repository\n\nAn existing `.git` directory makes the destination non-empty. Generate into\nthe current repository with `--force`:\n\n```bash\ncd pet-foster-family\nnpx --yes --package @craft-ts/dev-tools@beta craft create . --force\n```\n\n`--force` only permits writing into a non-empty destination; it does not turn\noff the configuration prompts. Review generated file changes before\ncommitting when the repository already contains application code.\n\nDuring the interactive flow, reference sources are cloned by default:\n\n- CraftTS sources go into `.references/craft-ts`;\n- EffectTS sources are also cloned when an Effect frontend or backend is\n selected;\n- the sources are available to agents without replacing the installed npm\n packages.\n\nAnswer `n` to opt out. In non-interactive mode, references remain opt-in so\nthat `--yes` does not silently perform network clones; use\n`--references=craft-ts` or `--references=all` explicitly.\n\nThe cloned repositories are reference material for coding agents only. The\ngenerated application always imports the published CraftTS and EffectTS npm\npackages from `package.json`; it does not use `file:` dependencies or\nTypeScript/Vite aliases to the clones. Use `npm run update:references` to fetch\nthe requested refs and refresh the recorded SHAs.\n\n## Non-interactive creation\n\nPass `--yes` after `create` to use defaults and disable all prompts. Combine it\nwith explicit options when the generated configuration must be reproducible:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --effect=none --agents=codex\n```\n\nFor a minimal plain starter:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --effect=none --i18n=none --design-system=none --no-typed-css \\\n --agents=none\n```\n\nTo create a backend-only Effect project and clone both reference sources:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --frontend-runtime=plain --backend-runtime=effect \\\n --references=all\n```\n\nThe main configuration options are:\n\n| Option | Values | Purpose |\n| -------------------- | ------------------------------------- | ----------------------------------------------------------------------- |\n| `--effect` | `v4`, `none` | Select the Effect v4 or plain starter |\n| `--frontend-runtime` | `plain`, `effect` | Choose the frontend runtime |\n| `--backend-runtime` | `none`, `promise`, `effect` | Choose server functions |\n| `--effect-scope` | `none`, `frontend`, `backend`, `both` | Set Effect placement |\n| `--agents` | comma-separated names or `none` | Add editor-specific agent integrations; `AGENTS.md` is always generated |\n| `--i18n` | `strict`, `loose`, `none` | Configure type-safe i18n |\n| `--design-system` | `basic`, `none` | Include the design-system starter |\n| `--typed-css` | flag / `--no-typed-css` | Enable or disable typed CSS |\n| `--workspace` | `standalone`, `nx` | Choose the workspace layout |\n| `--references` | `none`, `craft-ts`, `all` | Include source references |\n| `--no-demos` | flag | Generate a domain feature without explanatory demo pages |\n| `--domain` | slug | Name the first domain feature when using `--no-demos` |\n| `--force` | flag | Allow an existing non-empty destination |\n| `--json` | flag | Print the effective configuration as JSON |\n\nUse `craft create --help` to see the complete list:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create --help\n```\n\nFor a domain-first starting point, omit the explanatory home/services/about\npages and name the feature explicitly:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create pet-foster \\\n --yes --no-demos --domain animal --frontend-runtime=effect \\\n --backend-runtime=effect\n```\n\nThe generated feature lives under `src/app/features/animal/`. Add a form to\nthat feature with the existing primitives and its unit/submission test:\n\n```bash\ncraft add form animal\n# advanced nested/schema variant:\ncraft add form animal --advanced\n```\n\n## After generation\n\nThe generator creates a Git repository when the destination is not already\ninside another repository. It does not create a commit. The generated\n`.gitignore` excludes `node_modules/`, build outputs, test reports, and local\nreference clones.\n\nInstall dependencies and start the generated application:\n\n```bash\ncd my-app\nnpm install\nnpm run dev\n```\n\nThe generated project also includes the following checks:\n\n```bash\nnpm run lint\nnpm run typecheck\nnpm test\nnpm run architecture\nnpm run build\n```\n\nWith a backend, `src/server/application.ts` owns the registry and runtime\nLayer, while `src/server/node-http.ts` is only the Node stream adapter.\n`server.ts` re-exports both for compatibility. In the backend-only Effect\nprofile, the browser remains plain CraftTS; Effect services, middleware and\nerror projections stay under the server boundary.\n\n## Troubleshooting\n\n### `could not determine executable to run`\n\nIf the error mentions `craft@0.1.0`, `npx` resolved the unrelated public npm\npackage named `craft`. Use the explicit `--package @craft-ts/dev-tools@beta`\nform shown above.\n\nIf `@craft-ts/dev-tools` is already installed in the project, its local binary\ncan also be called with:\n\n```bash\nnpx craft create my-app\n```\n\nThe explicit form is still the safest command when bootstrapping a project\nthat has no `package.json` yet.\n"
181
186
  },
182
187
  {
183
188
  "path": "/guide/deployment",
@@ -207,7 +212,7 @@
207
212
  {
208
213
  "path": "/guide/forms",
209
214
  "title": "Forms",
210
- "body": "# Forms\n\nThere is no `FormBuilder` here. **A form is derived from a state** — its field\ntree, its validity and its error types are all consequences of that state and of\nthe mutation it submits to, so they cannot drift apart from them.\n\n**Use it when** you collect input that needs validation and a typed submission.\n**Not when** a single input maps to a single state — a plain\n[`state`](/guide/state/local-state) with a `set` is enough.\n\n::: tip Start with the guided version\n[Learn step 8](/learn/08-forms) builds a small form end to end before you dig\ninto the individual insertions.\n:::\n\n## Why it is shaped this way\n\nThree pillars, all of which follow from deriving rather than declaring:\n\n1. **Form Insertions** - Modular composition to tackle logic complexity\n2. **Type-safe errors** - Synchronous and asynchronous validation with type-safe exceptions (inferred from validators and submit handler)\n3. **Parallel Forms** - Support for multiple forms in the same state with automatic scoping\n\nAll of this is possible because the logic is entirely derived from the state.\n\n## Form Insertions\n\nForm insertions enable modular composition of functionality:\n\n### insertForm\n\nThe primary insertion that derives a typed form from a primitive.\n\n```ts\nimport { craftUse, state } from '@craft-ts/core';\nimport {\n insertForm,\n insertFormAttributes,\n insertNoopTypingAnchor,\n insertSelectFormTree,\n cRequired,\n cEmail,\n} from '@craft-ts/core';\n\nconst userFormState = craftUse(\n state(\n 'userFormState',\n { name: '', email: '' },\n insertForm(\n insertSelectFormTree(\n 'name',\n insertNoopTypingAnchor, // TS limitation\n insertFormAttributes(() => ({\n validators: [cRequired()],\n })),\n ),\n insertSelectFormTree(\n 'email',\n insertNoopTypingAnchor, // TS limitation\n insertFormAttributes(() => ({\n validators: [cRequired(), cEmail()],\n })),\n ),\n ),\n ),\n);\n\nconst form = userFormState.form;\nconst nameField = form.selectName();\nconst emailField = form.selectEmail();\n```\n\n> Note: It only works with the `state` primitive from now.\n\n> `insertNoopTypingAnchor` is a special insertion that does not add any logic but allows to anchor the typing of the form field. It is required for the form system to infer the correct types of fields and exceptions. (TS limitations...)\n\n### insertFormAttributes\n\nAdds attributes and validators to a form field.\n\n```ts\nconst formState = craftUse(\n state(\n 'formState',\n { email: '' },\n insertForm(\n insertSelectFormTree(\n 'email',\n insertNoopTypingAnchor,\n insertFormAttributes(() => ({\n validators: [cRequired(), cEmail()],\n disable: () => isLoading(),\n hidden: () => !showField(),\n })),\n ),\n ),\n ),\n);\n\n// Access email field and its exceptions\nconst form = formState.form;\nconst emailField = form.selectEmail();\nconst errors = emailField()().exceptions.list; // fully typed list of exceptions\nconst emailError = emailField()().exceptions.byValidator['cEmail'];\n```\n\n### Bind a field to the DOM\n\n`CraftFieldDirective` is the DOM adapter for a `CraftField`. It binds the field\nin both directions, marks it touched on blur, and reflects field state through\nnative attributes and `craft-*` CSS classes.\n\nIn a Craft template, apply the functional directive to the concrete node:\n\n```ts\nimport { CraftFieldDirective } from '@craft-ts/core';\n\ninput({\n type: 'email',\n}).pipe(CraftFieldDirective(loginForm.form.selectEmail()));\n```\n\n`insertSelectFormTree` materializes its branch lazily. When validators or other\ninsertions are attached through it, bind the field returned by `selectEmail()`\n(or the corresponding `selectXxx()` method). Binding the raw\n`loginForm.form.email` field bypasses that materialization, so those insertions\nare not registered.\n\nThe directive supports text inputs and textareas, numeric and temporal inputs,\ncheckboxes, radio groups and selects. Validators also project native constraints\nsuch as `required`, `min`, `max`, `minlength` and `maxlength`.\n\nFor a custom control, provide `CRAFT_FIELD_VALUE_CONTROL` or\n`CRAFT_FIELD_CHECKBOX_CONTROL` on the component root. Native Craft nodes use the\nfunctional directive directly.\n\n### Render validation exceptions exhaustively\n\n`fieldErrorNode.exhaustive` turns validation cases carried by\n`CraftFieldDirective` or exposed by the component logic into compile-time UI\nobligations. Every reachable code must have one handler, and an unreachable\nhandler is also rejected.\n\n```ts\nimport { fieldErrorNode, input, p } from '@craft-ts/component';\n\ninput({ id: 'email', type: 'email' })\n .pipe(CraftFieldDirective(loginForm.form.selectEmail()))\n .pipe(\n fieldErrorNode.exhaustive({\n required: () => p('Email is required.'),\n email: () => p('Enter a valid email.'),\n }),\n );\n```\n\nThe field stays mounted and invalid while a message is visible. The block adds\nand merges `aria-invalid` and `aria-describedby`; it does not throw an\nexception or feed route `handleExceptions`.\n\nUse `fieldErrorNode.partial` when only some codes belong near the field.\nHandled codes are removed from its contract and the remaining codes continue\nto the next field-exception boundary:\n\n```ts\ninput({ id: 'password', type: 'password' })\n .pipe(CraftFieldDirective(loginForm.form.selectPassword()))\n .pipe(\n fieldErrorNode.partial({\n required: () => p('Password is required.'),\n }),\n );\n```\n\nHere `password.required` is handled locally, while `password.minLength` must\nstill be handled by an enclosing `partial` or `exhaustive` block. A partial\nblock may omit reachable codes, but an unreachable handler remains a TypeScript\nerror.\n\nAt a component boundary, group handlers by static field path. Identical codes\non different fields remain separate obligations:\n\n```ts\nconst SafeLoginForm = BaseLoginForm.pipe(\n fieldErrorNode.exhaustive({\n email: {\n required: () => p('Email is required.'),\n email: () => p('Enter a valid email.'),\n },\n password: {\n required: () => p('Password is required.'),\n minLength: ({ exception }) =>\n p(`Use at least ${exception.payload} characters.`),\n },\n }),\n);\n```\n\nObject branches may also carry group or cross-field validators. Materialize the\nbranch in the component logic and return it from the factory:\n\n```ts\nconst credentials = registration.form.selectCredentials();\nreturn { registration, credentials };\n```\n\nIts cases, for example `credentials.passwordMismatch`, are part of the\ncomponent contract even when the group itself is not passed to\n`CraftFieldDirective`. Handle the grouped path on an enclosing template VNode\nor with `BaseComponent.pipe(fieldErrorNode.exhaustive(...))`. If it remains\nunhandled, rendering, mounting, and `loadCraftComponent` reject the component\nat compile time. See [Form exception handling](/guide/forms/exceptions) for the\ncomplete group example.\n\nBy default the block reads the field's `visibleExceptions` directly. The form\nowns that visibility policy; the default is touched or submitted:\n\n```ts\ninsertFormAttributes(() => ({\n validators: [cRequired(), cEmail()],\n exceptionVisibility: { anyOf: ['touched', 'submitted'] },\n}));\n```\n\nAfter a blur, only that field's visible exceptions are rendered. A submit\nattempt reveals the remaining exceptions for every field. Available states are\n`dirty`, `touched`, and `submitted`; a block can override\nthe inherited policy with `visibility: 'always'`, another `anyOf` combination,\nor a predicate. `mode` is `first` (validator order) or `all`, and `position` is\n`before` or `after`. Resetting the form clears dirty, touched, and submitted,\nso inherited messages are hidden again.\n\nCustom and async validators participate through their declared exception\nunion exactly like built-ins: their codes must be handled even when the current\nvisibility policy hides them.\n\n### insertFormSchema\n\nAdds a form-level `StandardSchemaV1` validator. Issues are projected onto the\nmatching fields by their schema path, while root and unmaterialized issues stay\navailable through `schemaExceptions()`.\n\n```ts\nconst formState = craftUse(\n state(\n 'formState',\n { email: '' },\n insertForm(insertFormSchema(userSchema), insertFormSubmit(saveUser)),\n ),\n);\nconst form = formState.form;\n\nform.email.errors();\nform.hasSchemaExceptions();\nform.schemaExceptions();\n```\n\nThe form keeps the schema input value. Schema transformations belong at the\nsubmit boundary, for example through the mutation's `methodSchema`.\n\n### insertFormSubmit\n\n`insertFormSubmit` connects the form to a mutation. It submits only validated\nform values and exposes the mutation's loading and typed exception state on the\nform.\n\nSee [Submitting a form](/guide/forms/submit) for the complete submission\nworkflow, including success handling and exception transformations.\n\n## The pages\n\n- **[Validation](/guide/forms/validation)** — built-in, custom and async validators\n- **[Submitting](/guide/forms/submit)** — wiring a form to a mutation, typed submit exceptions\n- **[Nested forms](/guide/forms/nested)** — sub-trees and sub-form fields\n- **[Exception handling](/guide/forms/exceptions)** — reading and shaping form errors\n- **[Complete examples](/guide/forms/examples)** — two forms end to end\n\n## See Also\n\n- [Validators](/guide/forms/validation)\n- [Submitting](/guide/forms/submit)\n- [Learn step 8](/learn/08-forms) — a form built end to end\n"
215
+ "body": "# Forms\n\nThere is no `FormBuilder` here. **A form is derived from a state** — its field\ntree, its validity and its error types are all consequences of that state and of\nthe mutation it submits to, so they cannot drift apart from them.\n\n**Use it when** you collect input that needs validation and a typed submission.\n**Not when** a single input maps to a single state — a plain\n[`state`](/guide/state/local-state) with a `set` is enough.\n\n::: tip Start with the guided version\n[Learn step 8](/learn/08-forms) builds a small form end to end before you dig\ninto the individual insertions.\n:::\n\n## Choose the insertion\n\nWhen a requirement mentions a form, start from `state` and add `insertForm`.\nThis map keeps the form tree, validation and mutation in one graph:\n\n| Need | Recommended API |\n| ------------------------------- | -------------------------------------- |\n| Form derived from state | `state` + `insertForm` |\n| Nested field or object branch | `insertSelectFormTree` |\n| Field attributes and validation | `insertFormAttributes` |\n| Whole-form validation | `insertFormSchema` |\n| Submit to a mutation | `insertFormSubmit` |\n| Field errors | `field.exceptions` or `fieldErrorNode` |\n| Submission state | `form().submitting()` |\n\n`insertNoopTypingAnchor` is only a type-inference anchor for a selected field;\nit adds no runtime behaviour. The common field shape is therefore:\n\n```ts\ninsertSelectFormTree(\n 'email',\n insertNoopTypingAnchor,\n insertFormAttributes(() => ({ validators: [cRequired(), cEmail()] })),\n);\n```\n\n## Native controls, one binding rule\n\n`CraftFieldDirective` supports text inputs, checkboxes, selects and textareas.\nKeep a stable `id`/`htmlFor` pair and put the directive on the native control:\n\n```ts\nimport { input, option, select, textarea } from '@craft-ts/component';\n\ninput('animal-name', { id: 'animal-name' }).pipe(\n CraftFieldDirective(animal.form.selectName()),\n);\n\ninput('animal-available', { id: 'animal-available', type: 'checkbox' }).pipe(\n CraftFieldDirective(animal.form.selectAvailable()),\n);\n\nselect('animal-species', { id: 'animal-species' }, [\n option('dog', { value: 'dog' }, 'Dog'),\n option('cat', { value: 'cat' }, 'Cat'),\n]).pipe(CraftFieldDirective(animal.form.selectSpecies()));\n\ntextarea('animal-notes', { id: 'animal-notes' }).pipe(\n CraftFieldDirective(animal.form.selectNotes()),\n);\n```\n\nA checkbox maps to a boolean, a select to its option value, and a textarea to\na string; nested fields use the corresponding selector chain.\n\n## A complete form in one file\n\nThis is the shortest complete path: typed state, required/email validation, a\nmutation, server exceptions, submitting state and errors rendered next to the\ncontrols. The text in a real application should come from its i18n catalogue.\n\n```ts\nimport {\n button,\n craftComponent,\n fieldErrorNode,\n form,\n input,\n label,\n p,\n} from '@craft-ts/component';\nimport {\n cEmail,\n cRequired,\n CraftFieldDirective,\n craftException,\n insertForm,\n insertFormAttributes,\n insertFormSubmit,\n insertNoopTypingAnchor,\n insertSelectFormTree,\n mutation,\n state,\n type ValidatedFormValue,\n} from '@craft-ts/core';\n\ntype Animal = { name: string; email: string };\n\nconst saveAnimal = mutation('saveAnimal', {\n method: (value: NonNullable<ValidatedFormValue<Animal>>) => value,\n loader: ({ params }) =>\n params.email.endsWith('@taken.test')\n ? craftException({ _tag: 'EMAIL_ALREADY_USED' }, { field: 'email' })\n : params,\n});\n\nexport const AnimalForm = craftComponent(\n 'AnimalForm',\n {},\n function* () {\n const animal = yield* state(\n 'animalForm',\n { name: '', email: '' } satisfies Animal,\n insertForm(\n insertSelectFormTree(\n 'name',\n insertNoopTypingAnchor,\n insertFormAttributes(() => ({ validators: [cRequired()] })),\n ),\n insertSelectFormTree(\n 'email',\n insertNoopTypingAnchor,\n insertFormAttributes(() => ({ validators: [cRequired(), cEmail()] })),\n ),\n insertFormSubmit(saveAnimal),\n ),\n );\n return { animal };\n },\n ({ animal }) =>\n form(\n 'animal-form',\n {\n *submit(event) {\n event.preventDefault();\n yield* animal.form.submit();\n },\n },\n [\n label({ htmlFor: 'animal-name' }, 'Name'),\n input('animal-name', { id: 'animal-name' })\n .pipe(CraftFieldDirective(animal.form.selectName()))\n .pipe(\n fieldErrorNode.exhaustive({\n required: () => p('Name is required.'),\n }),\n ),\n label({ htmlFor: 'animal-email' }, 'Email'),\n input('animal-email', { id: 'animal-email', type: 'email' })\n .pipe(CraftFieldDirective(animal.form.selectEmail()))\n .pipe(\n fieldErrorNode.exhaustive({\n required: () => p('Email is required.'),\n email: () => p('Enter a valid email.'),\n }),\n ),\n button(\n 'animal-submit',\n { type: 'submit', disabled: animal.form.submitting },\n 'Save',\n ),\n p(function* () {\n if (!(yield* animal.form.hasSubmitExceptions())) return '';\n return 'The server rejected this animal.';\n }),\n ],\n ),\n);\n```\n\nThe advanced version uses the same primitives for nested `address` fields,\nconditional visibility and asynchronous validation. Keep the branch insertion\nin the feature rather than hiding it in a component library; see\n[Nested forms](/guide/forms/nested) and\n[Validation](/guide/forms/validation#casyncvalidate).\n\n## Why it is shaped this way\n\nThree pillars, all of which follow from deriving rather than declaring:\n\n1. **Form Insertions** - Modular composition to tackle logic complexity\n2. **Type-safe errors** - Synchronous and asynchronous validation with type-safe exceptions (inferred from validators and submit handler)\n3. **Parallel Forms** - Support for multiple forms in the same state with automatic scoping\n\nAll of this is possible because the logic is entirely derived from the state.\n\n## Form Insertions\n\nForm insertions enable modular composition of functionality:\n\n### insertForm\n\nThe primary insertion that derives a typed form from a primitive.\n\n```ts\nimport { craftUse, state } from '@craft-ts/core';\nimport {\n insertForm,\n insertFormAttributes,\n insertNoopTypingAnchor,\n insertSelectFormTree,\n cRequired,\n cEmail,\n} from '@craft-ts/core';\n\nconst userFormState = craftUse(\n state(\n 'userFormState',\n { name: '', email: '' },\n insertForm(\n insertSelectFormTree(\n 'name',\n insertNoopTypingAnchor, // TS limitation\n insertFormAttributes(() => ({\n validators: [cRequired()],\n })),\n ),\n insertSelectFormTree(\n 'email',\n insertNoopTypingAnchor, // TS limitation\n insertFormAttributes(() => ({\n validators: [cRequired(), cEmail()],\n })),\n ),\n ),\n ),\n);\n\nconst form = userFormState.form;\nconst nameField = form.selectName();\nconst emailField = form.selectEmail();\n```\n\n> Note: It only works with the `state` primitive from now.\n\n> `insertNoopTypingAnchor` is a special insertion that does not add any logic but allows to anchor the typing of the form field. It is required for the form system to infer the correct types of fields and exceptions. (TS limitations...)\n\n### insertFormAttributes\n\nAdds attributes and validators to a form field.\n\n```ts\nconst formState = craftUse(\n state(\n 'formState',\n { email: '' },\n insertForm(\n insertSelectFormTree(\n 'email',\n insertNoopTypingAnchor,\n insertFormAttributes(() => ({\n validators: [cRequired(), cEmail()],\n disable: () => isLoading(),\n hidden: () => !showField(),\n })),\n ),\n ),\n ),\n);\n\n// Access email field and its exceptions\nconst form = formState.form;\nconst emailField = form.selectEmail();\nconst errors = emailField()().exceptions.list; // fully typed list of exceptions\nconst emailError = emailField()().exceptions.byValidator['cEmail'];\n```\n\n### Bind a field to the DOM\n\n`CraftFieldDirective` is the DOM adapter for a `CraftField`. It binds the field\nin both directions, marks it touched on blur, and reflects field state through\nnative attributes and `craft-*` CSS classes.\n\nIn a Craft template, apply the functional directive to the concrete node:\n\n```ts\nimport { CraftFieldDirective } from '@craft-ts/core';\n\ninput({\n type: 'email',\n}).pipe(CraftFieldDirective(loginForm.form.selectEmail()));\n```\n\n`insertSelectFormTree` materializes its branch lazily. When validators or other\ninsertions are attached through it, bind the field returned by `selectEmail()`\n(or the corresponding `selectXxx()` method). Binding the raw\n`loginForm.form.email` field bypasses that materialization, so those insertions\nare not registered.\n\nThe directive supports text inputs and textareas, numeric and temporal inputs,\ncheckboxes, radio groups and selects. Validators also project native constraints\nsuch as `required`, `min`, `max`, `minlength` and `maxlength`.\n\nFor a custom control, provide `CRAFT_FIELD_VALUE_CONTROL` or\n`CRAFT_FIELD_CHECKBOX_CONTROL` on the component root. Native Craft nodes use the\nfunctional directive directly.\n\n### Render validation exceptions exhaustively\n\n`fieldErrorNode.exhaustive` turns validation cases carried by\n`CraftFieldDirective` or exposed by the component logic into compile-time UI\nobligations. Every reachable code must have one handler, and an unreachable\nhandler is also rejected.\n\n```ts\nimport { fieldErrorNode, input, p } from '@craft-ts/component';\n\ninput({ id: 'email', type: 'email' })\n .pipe(CraftFieldDirective(loginForm.form.selectEmail()))\n .pipe(\n fieldErrorNode.exhaustive({\n required: () => p('Email is required.'),\n email: () => p('Enter a valid email.'),\n }),\n );\n```\n\nThe field stays mounted and invalid while a message is visible. The block adds\nand merges `aria-invalid` and `aria-describedby`; it does not throw an\nexception or feed route `handleExceptions`.\n\nUse `fieldErrorNode.partial` when only some codes belong near the field.\nHandled codes are removed from its contract and the remaining codes continue\nto the next field-exception boundary:\n\n```ts\ninput({ id: 'password', type: 'password' })\n .pipe(CraftFieldDirective(loginForm.form.selectPassword()))\n .pipe(\n fieldErrorNode.partial({\n required: () => p('Password is required.'),\n }),\n );\n```\n\nHere `password.required` is handled locally, while `password.minLength` must\nstill be handled by an enclosing `partial` or `exhaustive` block. A partial\nblock may omit reachable codes, but an unreachable handler remains a TypeScript\nerror.\n\nAt a component boundary, group handlers by static field path. Identical codes\non different fields remain separate obligations:\n\n```ts\nconst SafeLoginForm = BaseLoginForm.pipe(\n fieldErrorNode.exhaustive({\n email: {\n required: () => p('Email is required.'),\n email: () => p('Enter a valid email.'),\n },\n password: {\n required: () => p('Password is required.'),\n minLength: ({ exception }) =>\n p(`Use at least ${exception.payload} characters.`),\n },\n }),\n);\n```\n\nObject branches may also carry group or cross-field validators. Materialize the\nbranch in the component logic and return it from the factory:\n\n```ts\nconst credentials = registration.form.selectCredentials();\nreturn { registration, credentials };\n```\n\nIts cases, for example `credentials.passwordMismatch`, are part of the\ncomponent contract even when the group itself is not passed to\n`CraftFieldDirective`. Handle the grouped path on an enclosing template VNode\nor with `BaseComponent.pipe(fieldErrorNode.exhaustive(...))`. If it remains\nunhandled, rendering, mounting, and `loadCraftComponent` reject the component\nat compile time. See [Form exception handling](/guide/forms/exceptions) for the\ncomplete group example.\n\nBy default the block reads the field's `visibleExceptions` directly. The form\nowns that visibility policy; the default is touched or submitted:\n\n```ts\ninsertFormAttributes(() => ({\n validators: [cRequired(), cEmail()],\n exceptionVisibility: { anyOf: ['touched', 'submitted'] },\n}));\n```\n\nAfter a blur, only that field's visible exceptions are rendered. A submit\nattempt reveals the remaining exceptions for every field. Available states are\n`dirty`, `touched`, and `submitted`; a block can override\nthe inherited policy with `visibility: 'always'`, another `anyOf` combination,\nor a predicate. `mode` is `first` (validator order) or `all`, and `position` is\n`before` or `after`. Resetting the form clears dirty, touched, and submitted,\nso inherited messages are hidden again.\n\nCustom and async validators participate through their declared exception\nunion exactly like built-ins: their codes must be handled even when the current\nvisibility policy hides them.\n\n### insertFormSchema\n\nAdds a form-level `StandardSchemaV1` validator. Issues are projected onto the\nmatching fields by their schema path, while root and unmaterialized issues stay\navailable through `schemaExceptions()`.\n\n```ts\nconst formState = craftUse(\n state(\n 'formState',\n { email: '' },\n insertForm(insertFormSchema(userSchema), insertFormSubmit(saveUser)),\n ),\n);\nconst form = formState.form;\n\nform.email.errors();\nform.hasSchemaExceptions();\nform.schemaExceptions();\n```\n\nThe form keeps the schema input value. Schema transformations belong at the\nsubmit boundary, for example through the mutation's `methodSchema`.\n\n### insertFormSubmit\n\n`insertFormSubmit` connects the form to a mutation. It submits only validated\nform values and exposes the mutation's loading and typed exception state on the\nform.\n\nSee [Submitting a form](/guide/forms/submit) for the complete submission\nworkflow, including success handling and exception transformations.\n\n## The pages\n\n- **[Validation](/guide/forms/validation)** — built-in, custom and async validators\n- **[Submitting](/guide/forms/submit)** — wiring a form to a mutation, typed submit exceptions\n- **[Nested forms](/guide/forms/nested)** — sub-trees and sub-form fields\n- **[Exception handling](/guide/forms/exceptions)** — reading and shaping form errors\n- **[Complete examples](/guide/forms/examples)** — two forms end to end\n\n## See Also\n\n- [Validators](/guide/forms/validation)\n- [Submitting](/guide/forms/submit)\n- [Learn step 8](/learn/08-forms) — a form built end to end\n"
211
216
  },
212
217
  {
213
218
  "path": "/guide/forms/examples",
@@ -237,7 +242,7 @@
237
242
  {
238
243
  "path": "/guide/i18n",
239
244
  "title": "Type-safe i18n",
240
- "body": "# Type-safe i18n\n\n`@craft-ts/i18n` has **no CraftTS, Angular or Effect import**. The catalogue is\na plain TypeScript value and the runtime works in a browser, a server, a worker\nor a test without a framework. That is not a packaging detail — it is what lets\nthe same catalogue be checked by `tsc`, exercised by a Node test, and rendered\nduring SSR without a second implementation.\n\n## The contract\n\nFour things are guaranteed, and all four are checked before the app runs.\n\n| guarantee | what it costs you to break |\n| ----------------------------------------------------------- | ------------------------------------------------------------------- |\n| the key set is a **closed union** | an unknown key does not compile — no silent `order.totl` |\n| every locale has the **same keys with the same parameters** | a translation you forgot is a compile error, not a fallback |\n| parameters are **typed by their token** | a date cannot be passed where a currency amount belongs |\n| a plural carries **every category the locale requires** | Polish needs `one`/`few`/`many`/`other`; French needs `one`/`other` |\n\nThe usual failure mode of a translation layer is that all four of these are\nruntime concerns: a missing key renders its own name, a wrong parameter renders\n`[object Object]`, and a missing plural category renders the wrong branch to the\nusers of one locale only. None of that is observable from the code that calls\n`t`.\n\n## The shape of it\n\n```\nsrc/i18n/\n catalog.ts the reference locale — defineCatalog + msg + plural\n locales/fr-FR.ts every other locale — defineLocaleLike\n project-tokens.ts business tokens: defineToken / defineTokenFactory\n runtime.ts createI18nRuntime, and the reactive binding\n```\n\nA key is its dotted path: `order.total` reaches\n`{ order: { total: msg`…` } }`.\n\n## Where to go next\n\n- [The catalogue](./catalog.md) — `defineCatalog`, `msg`, `plural`,\n `defineLocale`, `defineLocaleLike`.\n- [Tokens](./tokens.md) — the shipped semantic tokens, and how to add your own.\n- [The runtime](./runtime.md) — `createI18nRuntime`, `t`, `bind`, lazy locales.\n- [With Effect](./effect.md) — `@craft-ts/i18n-effect`.\n\nTwo checks belong in CI, and `craft create` wires both:\n\n```bash\nnpm run i18n:check\nnpm run i18n:test\n```\n\nA working example lives in the demo, at `apps/demo/src/app/examples/i18n/`.\n"
245
+ "body": "# Type-safe i18n\n\n`@craft-ts/i18n` has **no CraftTS, Angular or Effect import**. The catalogue is\na plain TypeScript value and the runtime works in a browser, a server, a worker\nor a test without a framework. That is not a packaging detail — it is what lets\nthe same catalogue be checked by `tsc`, exercised by a Node test, and rendered\nduring SSR without a second implementation.\n\n## The contract\n\nFour things are guaranteed, and all four are checked before the app runs.\n\n| guarantee | what it costs you to break |\n| ----------------------------------------------------------- | ------------------------------------------------------------------- |\n| the key set is a **closed union** | an unknown key does not compile — no silent `order.totl` |\n| every locale has the **same keys with the same parameters** | a translation you forgot is a compile error, not a fallback |\n| parameters are **typed by their token** | a date cannot be passed where a currency amount belongs |\n| a plural carries **every category the locale requires** | Polish needs `one`/`few`/`many`/`other`; French needs `one`/`other` |\n\nThe usual failure mode of a translation layer is that all four of these are\nruntime concerns: a missing key renders its own name, a wrong parameter renders\n`[object Object]`, and a missing plural category renders the wrong branch to the\nusers of one locale only. None of that is observable from the code that calls\n`t`.\n\n## The shape of it\n\n```\nsrc/i18n/\n catalog.ts the reference locale — defineCatalog + msg + plural\n locales/fr-FR.ts every other locale — defineLocaleLike\n project-tokens.ts business tokens: defineToken / defineTokenFactory\n runtime.ts createI18nRuntime, and the reactive binding\n```\n\nA key is its dotted path: `order.total` reaches\n`{ order: { total: msg`…` } }`.\n\n## Where to go next\n\n- [The catalogue](./catalog.md) — `defineCatalog`, `msg`, `plural`,\n `defineLocale`, `defineLocaleLike`.\n- [Tokens](./tokens.md) — the shipped semantic tokens, and how to add your own.\n- [The runtime](./runtime.md) — `createI18nRuntime`, `t`, `bind`, lazy locales.\n- [With Effect](./effect.md) — `@craft-ts/i18n-effect`.\n\nTwo checks belong in CI, and `craft create` wires both:\n\n```bash\nnpm run i18n:check\nnpm run i18n:test\n```\n\nA working example lives in the demo, at `apps/demo/src/app/examples/i18n/`.\n\n## Guard visible text in Craft templates\n\nThe dev-tools plugin exposes an opt-in `i18n` ESLint preset for applications\nthat have finished moving their user-facing copy into a catalogue:\n\n```js\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n {\n plugins: { 'craft-ts': craftRules },\n rules: { ...craftRules.configs.i18n.rules },\n },\n];\n```\n\n`craft-ts/require-i18n-text` reports static text in visible headings,\nparagraphs, labels, buttons, links and options, plus visible `placeholder`,\n`aria-label` and `title` attributes. Dynamic business values, `i18n.t(...)`\nand catalogue files are accepted. Server files and tests are excluded so\ntechnical messages and assertions can remain literal. The rule is deliberately\nseparate from the recommended preset: enable it when the catalogue is the\napplication's source of truth, then run `npm run lint` in CI.\n"
241
246
  },
242
247
  {
243
248
  "path": "/guide/i18n/catalog",
@@ -287,7 +292,7 @@
287
292
  {
288
293
  "path": "/guide/reactivity/craft-method",
289
294
  "title": "craftMethod",
290
- "body": "# craftMethod\n\nWraps a generator so it can be called like an ordinary method — from a template,\nan event handler, anywhere outside the craft driver — while still resolving its\ndependencies with `yield*`.\n\n**Use it when** a click handler or a component method needs a service.\n**Not inside a craft factory** — there, `yield*` works directly.\n\n## Import\n\n```typescript\nimport { craftMethod } from '@craft-ts/core';\n```\n\n## Overview\n\n`craftMethod` is designed for component methods such as click handlers, submit handlers, and small UI orchestration callbacks.\n\nThe returned method carries the yieldable-method contract. When it is consumed\nfrom a Craft component template, its template view can delegate it with\n`yield*`; the component renderer drives the callback with the Craft generator\nruntime while preserving the method's injector and wrappers.\n\nThe method runs inside the injection context captured when `craftMethod(...)` is created.\n\nThat makes it useful when a component method needs to:\n\n- call Browser Boundaries with `yield*`\n- compose crafted services through `yield* SomeService()`\n- keep the handler colocated with component-local signals\n\n**All dependencies are cached, which helps to detect missing providers at compile time.**\n\n## Signatures\n\n```typescript\nfunction craftMethod<Name extends string, This, Args extends unknown[], Result>(\n name: Name,\n factory: (this: This, ...args: Args) => Generator<unknown, Result, unknown>,\n): (this: This, ...args: Args) => Result;\n\nfunction craftMethod<Name extends string, This, Args extends unknown[], Result>(\n name: Name,\n self: This,\n factory: (this: This, ...args: Args) => Generator<unknown, Result, unknown>,\n): (...args: Args) => Result;\n```\n\nThe first argument is the **name**: it is required and must match the\nproperty (or variable) the method is assigned to. It is the value used to tag\nthe injector context — same role as `provideHostName(...)`. The\n[`craft-ts/craft-method-name-match`](/guide/routing/eslint-rules) ESLint rule\nenforces the match and offers a quick fix.\n\n## The common case — inside a Craft component\n\nIn a Craft component's logic factory there is no `this`: declare the method with\n`craftMethod(name, fn)` and return it in the context.\n\n```typescript\nimport { button, craftComponent, div, p } from '@craft-ts/component';\nimport { Console, craftMethod, state } from '@craft-ts/core';\n\nexport const Counter = craftComponent(\n 'Counter',\n {},\n function* () {\n const counter = yield* state('counter', 0, ({ update }) => ({ update }));\n\n const increment = craftMethod('increment', function* (step = 1) {\n yield* Console.log('increment is called');\n yield* counter.update((value) => value + step);\n });\n\n return { counter, increment };\n },\n ({ counter, increment }) => [\n p(counter),\n button({ click: increment }, 'Increment'),\n ],\n);\n```\n\n\n\n`counter` does not belong to `increment`, so the method yields\n`counter.update`. Pass the method to the template (`click: increment`) rather\nthan wrapping `() => increment()`.\n\n## Composing crafted services\n\n`craftMethod` is not limited to Browser Boundaries — it consumes the same\ncrafted service graph as `craftService`:\n\n```typescript\nconst increment = craftMethod('increment', function* (value: number) {\n return yield* CounterWorker.set(value);\n});\n```\n\n::: details Class-based wrappers — capturing `this`\nWhen a class-based wrapper needs its instance, use one of the two `this`-aware\noverloads.\n\n### Recommended form — capture `this`\n\nUse `craftMethod(name, this, fn)` when the generator needs component state.\n\n```typescript\nimport { Console, craftMethod, craftSignal } from '@craft-ts/core';\n\nexport class Counter {\n readonly counter = craftSignal(0);\n\n readonly increment = craftMethod('increment', this, function* (step = 1) {\n yield* Console.log('increment is called');\n this.counter.update((value) => value + step);\n });\n}\n```\n\nThis overload captures the instance once, so the callback still works after extraction:\n\n```typescript\nconst increment = component.increment;\nincrement();\n```\n\n### Receiver-based form\n\nUse `craftMethod(name, fn)` when you want the method to resolve `this` from its receiver, and are fine with the receiver-dependent behavior.\n\nIn strict TypeScript, annotate `this` explicitly inside the generator:\n\n```typescript\nimport { Console, craftMethod, craftSignal } from '@craft-ts/core';\n\nexport class Counter {\n readonly counter = craftSignal(0);\n\n readonly increment = craftMethod(\n 'increment',\n function* (this: Counter, step = 1) {\n yield* Console.log('increment is called');\n this.counter.update((value) => value + step);\n return this.counter();\n },\n );\n}\n```\n\n### Composing services from a class\n\n```typescript\nexport class Counter {\n readonly increment = craftMethod(\n 'increment',\n this,\n function* (value: number) {\n return yield* CounterWorker.set(value);\n },\n );\n}\n```\n\n:::\n\n## Caveats\n\n- `craftMethod(...)` must be created inside an injection context, typically during component instantiation.\n- The first argument is a required name; it must match the property or variable name. The `craft-ts/craft-method-name-match` ESLint rule enforces this and provides a quick fix.\n- `craftMethod(name, fn)` depends on the receiver used at call time. If you extract the callback, `this` is no longer guaranteed unless you bind it yourself.\n- `craftMethod(name, this, fn)` is the recommended form whenever the generator reads or writes `this`.\n- `onAppStart(...)` is not supported inside `craftMethod`.\n\n## See Also\n\n- [`Browser Boundaries`](/guide/testing/browser-boundaries)\n- [`craftService`](/guide/app/craft-service)\n- [`onAppStart`](/guide/app/app-start)\n"
295
+ "body": "# craftMethod\n\nWraps a generator so it can be called like an ordinary method — from a template,\nan event handler, anywhere outside the craft driver — while still resolving its\ndependencies with `yield*`.\n\n**Use it when** a click handler or a component method needs a service.\n**Not inside a craft factory** — there, `yield*` works directly.\n\n## Import\n\n```typescript\nimport { craftMethod } from '@craft-ts/core';\n```\n\n## Overview\n\n`craftMethod` is designed for component methods such as click handlers and\nsubmit handlers. Keep the callback focused: event normalisation, pure input\npreparation, and at most one imperative Craft action belong here. When one\nevent must coordinate several primitives, emit a `source$` directly and let\nthe affected query react with `insertReactOnMutation(...)` or another\ndeclarative insertion.\n\nThe returned method carries the yieldable-method contract. When it is consumed\nfrom a Craft component template, its template view can delegate it with\n`yield*`; the component renderer drives the callback with the Craft generator\nruntime while preserving the method's injector and wrappers.\n\nThe method runs inside the injection context captured when `craftMethod(...)` is created.\n\nThat makes it useful when a component method needs to:\n\n- call Browser Boundaries with `yield*`\n- compose crafted services through `yield* SomeService()`\n- keep the handler colocated with component-local signals\n\n**All dependencies are cached, which helps to detect missing providers at compile time.**\n\n## Signatures\n\n```typescript\nfunction craftMethod<Name extends string, This, Args extends unknown[], Result>(\n name: Name,\n factory: (this: This, ...args: Args) => Generator<unknown, Result, unknown>,\n): (this: This, ...args: Args) => Result;\n\nfunction craftMethod<Name extends string, This, Args extends unknown[], Result>(\n name: Name,\n self: This,\n factory: (this: This, ...args: Args) => Generator<unknown, Result, unknown>,\n): (...args: Args) => Result;\n```\n\nThe first argument is the **name**: it is required and must match the\nproperty (or variable) the method is assigned to. It is the value used to tag\nthe injector context — same role as `provideHostName(...)`. The\n[`craft-ts/craft-method-name-match`](/guide/routing/eslint-rules) ESLint rule\nenforces the match and offers a quick fix.\n\n## The common case — inside a Craft component\n\nIn a Craft component's logic factory there is no `this`: declare the method with\n`craftMethod(name, fn)` and return it in the context.\n\n```typescript\nimport { button, craftComponent, div, p } from '@craft-ts/component';\nimport { Console, craftMethod, state } from '@craft-ts/core';\n\nexport const Counter = craftComponent(\n 'Counter',\n {},\n function* () {\n const counter = yield* state('counter', 0, ({ update }) => ({ update }));\n\n const increment = craftMethod('increment', function* (step = 1) {\n yield* Console.log('increment is called');\n yield* counter.update((value) => value + step);\n });\n\n return { counter, increment };\n },\n ({ counter, increment }) => [\n p(counter),\n button({ click: increment }, 'Increment'),\n ],\n);\n```\n\n`counter` does not belong to `increment`, so the method yields\n`counter.update`. Pass the method to the template (`click: increment`) rather\nthan wrapping `() => increment()`.\n\n## Composing crafted services\n\n`craftMethod` is not limited to Browser Boundaries — it consumes the same\ncrafted service graph as `craftService`:\n\n```typescript\nconst increment = craftMethod('increment', function* (value: number) {\n return yield* CounterWorker.set(value);\n});\n```\n\n::: details Class-based wrappers — capturing `this`\nWhen a class-based wrapper needs its instance, use one of the two `this`-aware\noverloads.\n\n### Recommended form — capture `this`\n\nUse `craftMethod(name, this, fn)` when the generator needs component state.\n\n```typescript\nimport { Console, craftMethod, craftSignal } from '@craft-ts/core';\n\nexport class Counter {\n readonly counter = craftSignal(0);\n\n readonly increment = craftMethod('increment', this, function* (step = 1) {\n yield* Console.log('increment is called');\n this.counter.update((value) => value + step);\n });\n}\n```\n\nThis overload captures the instance once, so the callback still works after extraction:\n\n```typescript\nconst increment = component.increment;\nincrement();\n```\n\n### Receiver-based form\n\nUse `craftMethod(name, fn)` when you want the method to resolve `this` from its receiver, and are fine with the receiver-dependent behavior.\n\nIn strict TypeScript, annotate `this` explicitly inside the generator:\n\n```typescript\nimport { Console, craftMethod, craftSignal } from '@craft-ts/core';\n\nexport class Counter {\n readonly counter = craftSignal(0);\n\n readonly increment = craftMethod(\n 'increment',\n function* (this: Counter, step = 1) {\n yield* Console.log('increment is called');\n this.counter.update((value) => value + step);\n return this.counter();\n },\n );\n}\n```\n\n### Composing services from a class\n\n```typescript\nexport class Counter {\n readonly increment = craftMethod(\n 'increment',\n this,\n function* (value: number) {\n return yield* CounterWorker.set(value);\n },\n );\n}\n```\n\n:::\n\n## Caveats\n\n- `craftMethod(...)` must be created inside an injection context, typically during component instantiation.\n- The first argument is a required name; it must match the property or variable name. The `craft-ts/craft-method-name-match` ESLint rule enforces this and provides a quick fix.\n- `craftMethod(name, fn)` depends on the receiver used at call time. If you extract the callback, `this` is no longer guaranteed unless you bind it yourself.\n- `craftMethod(name, this, fn)` is the recommended form whenever the generator reads or writes `this`.\n- `onAppStart(...)` is not supported inside `craftMethod`.\n\n## See Also\n\n- [`Browser Boundaries`](/guide/testing/browser-boundaries)\n- [`craftService`](/guide/app/craft-service)\n- [`onAppStart`](/guide/app/app-start)\n"
291
296
  },
292
297
  {
293
298
  "path": "/guide/reactivity/from-event-to-source",
@@ -317,7 +322,7 @@
317
322
  {
318
323
  "path": "/guide/routing/eslint-rules",
319
324
  "title": "ESLint rules",
320
- "body": "# ESLint rules\n\nThe rule set is not decoration: several checks in this documentation only work\nbecause a rule generated or maintained the code they read. Others enforce the\narchitecture — no hidden runtime dependencies or direct transport calls — and most of them\n**autofix**.\n\n**Install them once** when you set up routing and type-safe DI.\n**Then lean on the quick fixes** rather than writing the boilerplate by hand.\n\n::: warning An ESLint error is not a compile error\nA missing autofix does not break the build. If you skip the quick fix after\nchanging a component's DI shape, `main.ts` keeps reading a stale `GenDeps_*` and\ncan miss a real DI error. Run `eslint --fix` in CI.\n:::\n\nThe plugin is exposed from `@craft-ts/dev-tools/eslint-rules`.\n\nFor a project using `@craft-ts/effect`, the published preset enables the Craft\nrules and the Effect adapter rule in one entry:\n\n```ts\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n {\n files: ['**/*.ts'],\n ...craftRules.configs.effect,\n },\n];\n```\n\nUse `craftRules.configs.recommended` for projects that do not use Effect.\n\nAdd it to your ESLint flat config:\n\n```ts\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n // keep your existing ESLint config entries\n {\n files: ['**/*.ts'],\n plugins: {\n 'craft-ts': craftRules,\n },\n rules: {\n 'craft-ts/prefer-craft-template-blocks': 'error',\n 'craft-ts/no-render-writes': 'error',\n 'craft-ts/require-reactive-template-bindings': 'error',\n 'craft-ts/no-craft-use': 'error',\n 'craft-ts/no-type-assertions-in-template': 'error',\n 'craft-ts/no-ephemeral-template-form-state': 'error',\n 'craft-ts/template-element-name-unique': 'error',\n 'craft-ts/no-craft-computed-side-effects': 'error',\n 'craft-ts/require-craft-method-for-yieldable-callback': 'error',\n 'craft-ts/prefer-direct-yieldable-callback': 'error',\n 'craft-ts/require-yieldable-reactive-read': 'error',\n 'craft-ts/require-yieldable-template-method': 'error',\n 'craft-ts/require-yieldable-insertion-write': 'error',\n 'craft-ts/no-craft-service-component-same-file': 'error',\n 'craft-ts/prefer-craft-http-transport': 'error',\n 'craft-ts/no-injection-token': 'error',\n 'craft-ts/require-primitive-derived-property': 'error',\n 'craft-ts/no-async-await': 'error',\n 'craft-ts/no-throw': 'error',\n 'craft-ts/no-imperative-craft-resource-trigger': 'error',\n 'craft-ts/no-transition-actions': 'error',\n 'craft-ts/require-craft-resource-trigger-yield': 'error',\n 'craft-ts/require-assert-exhaustive-route-exceptions': 'error',\n 'craft-ts/require-craft-exception-handler': 'error',\n 'craft-ts/require-exception-component-di-check': 'error',\n 'craft-ts/require-pending-component-di-check': 'error',\n 'craft-ts/require-child-route-mount-check': 'error',\n 'craft-ts/require-lazy-load-with-retry': 'error',\n 'craft-ts/require-cascade-route-di-check': 'error',\n 'craft-ts/global-exception-registry-match': 'error',\n },\n },\n];\n```\n\nWhat each rule does:\n\n- `craft-ts/prefer-craft-template-blocks`: keeps `craftComponent(...)` templates declarative by rejecting ternaries, logical expressions, negations, and imperative control flow; use `ifNode(...)`, `matchNode.exhaustive(...)`, `forNode(...)`, or `deferNode(...)`\n- `craft-ts/no-render-writes`: rejects detectable `set()`, `update()`, and `mutate()` calls in component templates and render bindings while allowing DOM event and `onXxx` output callbacks\n- `craft-ts/require-reactive-template-bindings`: requires signals, named Craft values, and component inputs to be read inside granular binding callbacks instead of during VNode construction; static values remain valid\n- `craft-ts/no-craft-use`: forbids the synchronous `craftUse(...)` escape hatch in Craft TypeScript files; use a generator and delegate the reader with `yield*` instead\n- `craft-ts/no-type-assertions-in-template`: forbids `as ...` and angle-bracket type assertions in Craft templates; fix the type in the logic factory or expose a correctly typed derived value\n- `craft-ts/no-ephemeral-template-form-state`: forbids `let` / `const` / `var` in the fourth argument of `craftComponent(...)` and `craftDirective(...)` (inline or a same-file identifier). Declare that state in the logic factory with `state()` or `craftComputed()` instead\n- `craft-ts/template-element-name-unique`: requires named HTML helpers to use a static, unique local name within a component; use the object-first helper form for unnamed elements such as `p({ id: 'hint' }, ...)`\n- `craft-ts/no-craft-computed-side-effects`: forbids writes and asynchronous work inside `craftComputed`; only reactive reads and `settled(...)` are allowed. The graph-wide counterpart is [`assertCraftComputedPure`](/guide/testing/architecture#assertcraftcomputedpure).\n- `craft-ts/no-effect-outside-loaders`: keeps `params`, methods, `craftComputed(...)`, and `craftEffect(...)` synchronous by allowing Effect values and Effect service reads only in Effect loaders; `no-effect-in-params` remains as a compatibility alias\n- `craft-ts/sync-effect-body`: keeps a body declared synchronous (`SyncOp` in its requirements) free of anything that may suspend — async constructors such as `Effect.sleep`/`Effect.promise`, and members nothing declares synchronous. Type-aware: the ESLint parser must use `projectService: true` or a TypeScript `project`\n- `craft-ts/no-explicit-effect-type`: lets `Effect.gen` infer its complete type instead of repeating an explicit Effect annotation; contracts declared in interfaces and type aliases remain allowed\n- `craft-ts/prefer-inline-effect-insertion`: keeps the `queryEffect` insertion factory inline so its resource and exception types are inferred without a separate `InsertionParams` context alias\n- `craft-ts/prefer-inline-route-providers`: inlines a route provider tuple used only once by `loadCraftComponent(...)`, preserving the route-level type proof\n- `craft-ts/prefer-craft-reactivity`: rejects authored signal/computed/effect/resource APIs, explicit `.subscribe()` calls, and RxJS `Subject`/`BehaviorSubject`/`ReplaySubject`; use `state`, `craftComputed`, `craftEffect`, `query`, and named `source$`/`on$` flows\n- `craft-ts/prefer-craft-service`: keeps services in the `craftService(...)` model\n- `craft-ts/no-craft-service-component-same-file`: forbids declaring `craftService(...)` and `craftComponent(...)` in the same file; a route-level service provider combined with a lazy-loaded component can break lazy loading, so keep them in separate files\n- `craft-ts/no-injection-token`: forbids authored `InjectionToken` contracts; declare them with `craftService({ name, providedIn: 'abstract' }, abstract<Contract>())`\n- `craft-ts/prefer-craft-http-client`: forbids direct transport usage in favor of `CraftHttpClient`\n- `craft-ts/prefer-craft-http-transport`: forbids direct `fetch()` and `XMLHttpRequest`; use `query()` for reads or `mutation()` for writes with `CraftHttpClient`\n- `craft-ts/prefer-craft-input-output`: keeps component inputs and outputs in the `Input`/`Output` model used by `craftComponent(...)`\n- `craft-ts/require-primitive-derived-property`: requires a `computed` or `craftComputed` that only depends on one primitive in the same component/service to be exposed by that primitive's insertion; simple cases are autofixed\n- `craft-ts/no-async-await`: forbids `async` functions, `await`, and `for await...of`; use generator-based Craft primitives, `craftSleep`, and `CraftHttpClient` instead\n- `craft-ts/no-throw`: forbids `throw` in Craft code and offers a Quick Fix that returns `craftException({ _tag: 'UNEXPECTED_ERROR' }, { error: ... })`; keep technical boundaries and tests outside this rule when their contracts require thrown errors\n- `craft-ts/no-imperative-craft-resource-trigger`: forbids `query.call(...)`, `mutation.mutate(...)`, and `asyncProcess.method(...)` in a `craftEffect` dependency graph, including through `craftGen(...)`. The graph-wide counterpart, including `state` / `source$` writes, is [`assertCraftEffectNoImperativeSync`](/guide/testing/architecture#assertcrafteffectnoimperativesync).\n- `craft-ts/no-transition-actions`: forbids `query.call(...)`, `mutation.mutate(...)`, and `asyncProcess.method(...)` inside `transitionStep(...)`; validate the event and emit a source, then let the resource react to that source.\n- `craft-ts/require-craft-resource-trigger-yield`: requires those triggers to use `yield*` inside generator functions, while ordinary UI callbacks may keep imperative calls\n- `craft-ts/require-craft-method-for-yieldable-callback`: requires callbacks returned by a `craftComponent` factory to wrap yieldable Craft method calls in `craftMethod(...)`\n- `craft-ts/prefer-direct-yieldable-callback`: replaces a template generator or generator method that only delegates `yield* callback()` with the callback reference itself (`callback` or `object.method`)\n- `craft-ts/require-yieldable-reactive-read`: requires Craft reactive readers to be delegated with `yield*` inside generator functions; a function that reads a Craft reader must itself be a generator\n- `craft-ts/require-yieldable-template-method`: requires yieldable Craft method calls in a `craftComponent` template to be delegated with `yield*`, or passed as a reference (`click: counter.increment`)\n- `craft-ts/require-yieldable-insertion-write`: requires `set(...)`, `patch(...)`, and `update(...)` to be delegated with `yield*` when they are used inside a generator method\n- `craft-ts/require-assert-exhaustive-route-exceptions`: adds the collection-level `assertExhaustiveRouteExceptions(...)` safety net\n- `craft-ts/require-craft-exception-handler`: enforces `craftExceptionHandler(function* (...) {})`; simple handlers are autofixed and ambiguous raw redirects are reported for manual migration\n- `craft-ts/require-exception-component-di-check`: generates O(1) `RouteExceptionComponentCheckedDI` checks for `renderComponent`, route-level `errorComponent`, `withErrorComponent`, `withRouteLoadError`, and route-local `provideRouteLoadErrorComponent`\n- `craft-ts/require-pending-component-di-check`: generates the independent `RouteCheckedDI` check for each `pendingComponent`\n- `craft-ts/no-raw-class`: forbids a `class:` binding that is a string, a template literal or a function, in any file that imports `@craft-ts/style`. A class assembled at render time is a visual state nothing recorded, so the [visual matrix](/guide/style/testing) would enumerate what the sheets declare while the DOM shows something else. Move the rule into the sheet and bind the class it returns; make the variation an axis and set a `data-*` attribute\n- `craft-ts/no-raw-css-value`: forbids a string or number literal as an argument to a `@craft-ts/style` helper — `p('12px')`, `bg('red')`. If the scale is missing the step, add it to the scale; if the value genuinely cannot be proven, `unsafeLength('13px', reason)` compiles and makes the debt countable in the [graph](/guide/style/testing#what-the-graph-adds)\n- `craft-ts/no-free-has`: forbids a hand-written `:has()` in styles. It reaches across the component boundary, so what a component looks like depends on markup it does not own — a state the matrix cannot enumerate. Use the `descendant` axis, which is a closed set and carries its own test driver\n- `craft-ts/style-file-boundary`: restricts a `*.style.ts` to style-vocabulary imports. The [build plugin](/guide/style/setup) imports the file in Node to read what it registered, so an application import would run application code at build time\n- `craft-ts/craft-css-token-registry`: reports a custom property registered with `@property` by two different components. A custom property may have only one owner; two silently fight over its syntax and initial value\n- `craft-ts/require-effect-adapters`: requires the Effect-aware adapters — `queryEffect`, `mutationEffect`, `asyncProcessEffect` — instead of the plain primitives in an Effect application. See [Choose the right adapter](/guide/advanced/effect#choose-the-right-adapter)\n- `craft-ts/craft-signal-source-name-match`: requires `signalSource(name, ...)` to take a string literal matching the variable, class property or object property it is assigned to, so the name in a trace is the name in the source. A computed name defeats the [architecture graph](/guide/testing/architecture), which reads these names statically\n- `craft-ts/require-child-route-mount-check`: adds the missing `assertChildRouteMounts(...)` call + import (Quick Fix) for any `craftRoutes(...)` collection that mounts lazy `loadChildren`, so a `.withParent`-pinned child mounted under the wrong path is a compile error\n- `craft-ts/require-lazy-load-with-retry`: wraps route `loadComponent` and `loadChildren` imports with the generated `withRetry(...)` loader helper while preserving a statically analyzable import specifier\n- `craft-ts/require-cascade-route-di-check`: rejects any `craftRoutes(...)` collection without a same-file `ValidateCascadeRoutesFile + CanRun` proof; its autofix adds the conservative `<never, Router>` context, which should be adjusted when the mount inherits providers\n- `craft-ts/global-exception-registry-match`: keeps `CraftGlobalExceptionRegistry` synchronized with handlers delegating to `globalError()`\n\n### Accessibility (`craft-ts/a11y`)\n\nSpread `craftRules.configs.a11y.rules` to enable the WCAG 2.2 AA preset as\n`error`. The rules walk **all** hyperscript in the file (`craftTemplate`,\nextracted factories, `h('tag')`), not only `craftComponent` argument 3.\n\n- `prefer-named-html-helpers`: forbids `h('img')` / `h('button')` when a named helper exists\n- `require-interactive-local-name`: requires a string-literal first argument on interactive helpers; the local name is the third segment of `data-craft-name=\"${component}:${tag}:${localName}\"`\n- `img-has-alt`, `iframe-has-title`, `button-has-type`, `anchor-has-href`\n- `control-has-accessible-name`, `label-has-associated-control`, `heading-has-content`\n- `no-noninteractive-element-interactions`, `no-positive-tabindex`\n- `valid-aria`, `role-has-required-aria`, `target-blank-noopener`\n- `prefer-relative-heading`, `require-route-heading-outline`,\n `require-outlet-heading-section`, `no-heading-level-skip`\n- `require-focus-visible`, `require-reduced-motion` (CSS of `craftComponent`)\n\nSee [Accessibility](/guide/components/accessibility).\n\nThe two migration rules also expose a VS Code ESLint Quick Fix suggestion that inserts a temporary local disable comment with the intended migration note when you need to unblock a file before doing the full refactor.\n\nThe template and reactivity rules are intentionally diagnostic-only: replacing a\nresource or subscription can change lifecycle and error semantics, so the rule\npoints at the Craft primitive without applying a potentially unsafe rewrite.\n\n### Why templates use blocks\n\nCraft template blocks preserve the branch structure in the type-level render\ncontract. A ternary or `condition && node` produces only a computed value, so\nthe type checker cannot assert which branch renders which content. Keep derived\nvalues and business decisions in the component's state/query layer, then make\nthe template express visibility explicitly:\n\n```ts\nifNode(\n isReady,\n () => p('Ready'),\n () => p('Loading…'),\n);\n\nmatchNode.exhaustive(query.exceptions, '_tag', {\n NOT_FOUND: () => p('Not found'),\n FORBIDDEN: () => p('Forbidden'),\n});\n```\n\nThis rule is for Craft's TypeScript templates. It does not rewrite external\ntemplate languages.\n\nThe same restriction applies to boolean expressions. A negation is still\napplication logic, even when it is used only for a DOM property:\n\n```ts\n// Incorrect: the template derives the disabled state.\nbutton(\n {\n disabled: function* () {\n return !(yield* machine.canGoBack());\n },\n },\n 'Back',\n);\n\n// Correct: derive it in the logic factory and bind the result.\nconst backDisabled = craftComputed('backDisabled', function* () {\n return !(yield* history.canGoBack());\n});\nreturn { backDisabled };\n```\n\nKeep the template to layout and binding. Move labels, formatted values,\nvalidation state, and other decisions into `state()` or `craftComputed()`.\n\n### Derived values belong to their primitive\n\nWhen a computed reads only one local primitive, declare it in that primitive's\ninsertion. This keeps the dependency visible and lets pending/exception\nboundaries name the actual source:\n\n```ts\nconst users =\n yield *\n query('users', config, ({ resource }) => ({\n total: craftComputed('total', function* () {\n return (yield* settled(resource)).length;\n }),\n }));\n```\n\nDo not create `craftComputed('total', ...)` beside the query when the\ncomputation depends only on `users`.\n\n### Keep casts and synchronous reads out of templates\n\nCraft templates reject both `as ...` / angle-bracket assertions and\n`craftUse(...)`. Fix the type or perform the synchronous-to-reactive\nconversion in the component logic, then expose a typed reader or generator to\nthe template:\n\n```ts\nconst typedStep = machine.stepState as unknown as () => { step: Step };\nreturn { typedStep };\n\n// Template: no cast and no craftUse.\nmatchNode.exhaustive(typedStep, 'step', steps);\n```\n\n`no-craft-use` applies to Craft TypeScript files, not only the fourth\n`craftComponent(...)` argument. A synchronous integration boundary may opt out\nlocally when its external API cannot consume a generator, but application\nstate and templates should use `yield*`.\n\n### Form and accessibility diagnostics\n\nThe accessibility preset also checks the static structure of hyperscript:\n\n- give every `label` an `htmlFor` matching the control `id`, or wrap the control;\n- give named controls and helpers a unique string local name;\n- use `button` or `a` for interactions instead of adding `click` to a `div`;\n- add a `prefers-reduced-motion` branch whenever component CSS defines an\n animation or transition.\n\nThese checks run on Craft TypeScript templates and extracted helper factories,\nso moving markup into a local function does not bypass them.\n\n### Reactive values belong in binding callbacks\n\n`require-reactive-template-bindings` uses TypeScript type information to find\nreactive reads. Reading a signal while constructing a VNode would make it a\ndependency of the structural component render, so the rule rejects this form:\n\n```ts\n// Incorrect: count is read by the component template.\np(`Count: ${count()}`);\nbutton({ disabled: isDisabled() }, 'Save');\ndiv({ class: { active: isActive() } });\n```\n\nKeep each read inside the callback owned by its DOM binding. Pass a yieldable\nreader, or use a generator when the binding must format:\n\n```ts\np(count);\np(function* () {\n return `Count: ${yield* count()}`;\n});\nbutton({ disabled: isDisabled }, 'Save');\ndiv({ class: isActiveClass });\n```\n\nLiteral and otherwise static values are still allowed, as are reads performed\nfrom DOM events and `onXxx` output callbacks. Because the rule is type-aware,\nthe ESLint parser must use `projectService: true` or a TypeScript `project`.\n\n### Pass simple yieldable callbacks directly\n\n`prefer-direct-yieldable-callback` removes a generator wrapper when the\ntemplate only delegates one zero-argument callback. It handles both a value\nbinding and a generator method:\n\n```ts\n// Before: redundant wrappers around the callbacks.\nbutton(\n {\n *click() {\n yield* press();\n },\n },\n function* () {\n return yield* label();\n },\n);\n\n// After `eslint --fix`.\nbutton({ click: press }, label);\n```\n\nMember callbacks are supported as well when the access is static and has no\narguments:\n\n```ts\n// Before.\nspan(function* () {\n return yield* counter.increment();\n});\n\n// After.\nspan(counter.increment);\n```\n\nThe rule leaves callbacks with parameters, extra statements, or additional\ncomputation unchanged. In those cases the generator contains behavior that\ncannot be represented by passing the callback reference alone.\n\n### Yield insertion writes from generator methods\n\n`require-yieldable-insertion-write` requires `set(...)`, `patch(...)`, and\n`update(...)` calls to be delegated with `yield*` when they are used inside a\ngenerator method:\n\n```ts\nnextPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page + 1 });\n},\n```\n\nInsertion callbacks that are not generators may return a write directly; the\ninsertion wrapper consumes that result for them.\n\n## What generates what\n\nThree rules do more than complain — they write code you would otherwise\nmaintain by hand:\n\n| Rule | Generates |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `require-cascade-route-di-check` | the same-file DI proof for a `craftRoutes(...)` collection |\n| `require-assert-exhaustive-route-exceptions` | the collection-level exhaustiveness assert |\n| `require-child-route-mount-check` | the `assertChildRouteMounts(...)` call and its import |\n| `require-lazy-load-with-retry` | the `withRetry(...)` wrapper on lazy route imports |\n| `prefer-direct-yieldable-callback` | replaces redundant generators with direct callback references |\n\n## Adopting them progressively\n\nOn an existing codebase, enable them in waves rather than all at once:\n\n1. **The route safety nets** — the `require-*` rules. Mostly autofixable. They\n generate the proofs; [architecture tests](/guide/testing/architecture#assertroutediproofs)\n (`assertRouteDiProofs`) fail CI if a proof is later removed or left unarmed.\n2. **The architecture rules last** — `prefer-craft-service`,\n `no-craft-service-component-same-file`, `prefer-craft-http-client`,\n `require-yieldable-reactive-read`,\n `require-yieldable-template-method`, `require-yieldable-insertion-write`.\n These ask for real refactors.\n\nThe four style rules — `no-raw-class`, `no-raw-css-value`, `no-free-has`,\n`style-file-boundary` — are in `craftRules.configs.recommended` at `'error'`,\nand they are **gated on the import**: they fire only in files that import\n`@craft-ts/style`. A component you have not migrated is not claiming the\nguarantee, so nothing reports it. The day a file starts using the design system\nis the day it starts being held to it — which is why enabling them on an\nunmigrated codebase costs nothing.\n\nThe two migration rules also expose a VS Code quick fix that inserts a temporary\nlocal disable comment with the intended migration note, so you can unblock a\nfile before doing the full refactor.\n\n## See Also\n\n- [Routing setup](/guide/routing/setup) — where these rules are installed\n- [CLI automation](/guide/routing/automation) — the codemods they complement\n- [Architecture rules](/guide/testing/architecture) — graph-wide constraints ESLint cannot see\n- [Activating the style system](/guide/style/setup) — what the four style rules are guarding\n"
325
+ "body": "# ESLint rules\n\nThe rule set is not decoration: several checks in this documentation only work\nbecause a rule generated or maintained the code they read. Others enforce the\narchitecture — no hidden runtime dependencies or direct transport calls — and most of them\n**autofix**.\n\n**Install them once** when you set up routing and type-safe DI.\n**Then lean on the quick fixes** rather than writing the boilerplate by hand.\n\n::: warning An ESLint error is not a compile error\nA missing autofix does not break the build. If you skip the quick fix after\nchanging a component's DI shape, `main.ts` keeps reading a stale `GenDeps_*` and\ncan miss a real DI error. Run `eslint --fix` in CI.\n:::\n\nThe plugin is exposed from `@craft-ts/dev-tools/eslint-rules`.\n\nFor a project using `@craft-ts/effect`, the published preset enables the Craft\nrules and the Effect adapter rule in one entry:\n\n```ts\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n {\n files: ['**/*.ts'],\n ...craftRules.configs.effect,\n },\n];\n```\n\nUse `craftRules.configs.recommended` for projects that do not use Effect.\n\nAdd it to your ESLint flat config:\n\n```ts\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n // keep your existing ESLint config entries\n {\n files: ['**/*.ts'],\n plugins: {\n 'craft-ts': craftRules,\n },\n rules: {\n 'craft-ts/prefer-craft-template-blocks': 'error',\n 'craft-ts/no-render-writes': 'error',\n 'craft-ts/require-reactive-template-bindings': 'error',\n 'craft-ts/no-craft-use': 'error',\n 'craft-ts/no-type-assertions-in-template': 'error',\n 'craft-ts/no-ephemeral-template-form-state': 'error',\n 'craft-ts/template-element-name-unique': 'error',\n 'craft-ts/no-craft-computed-side-effects': 'error',\n 'craft-ts/require-craft-method-for-yieldable-callback': 'error',\n 'craft-ts/prefer-direct-yieldable-callback': 'error',\n 'craft-ts/require-yieldable-reactive-read': 'error',\n 'craft-ts/require-yieldable-template-method': 'error',\n 'craft-ts/require-yieldable-insertion-write': 'error',\n 'craft-ts/no-craft-service-component-same-file': 'error',\n 'craft-ts/prefer-craft-http-transport': 'error',\n 'craft-ts/no-injection-token': 'error',\n 'craft-ts/require-primitive-derived-property': 'error',\n 'craft-ts/no-async-await': 'error',\n 'craft-ts/no-throw': 'error',\n 'craft-ts/no-imperative-craft-resource-trigger': 'error',\n 'craft-ts/no-imperative-craft-method-actions': 'error',\n 'craft-ts/no-imperative-storage-in-craft-method': 'error',\n 'craft-ts/no-transition-actions': 'error',\n 'craft-ts/require-craft-resource-trigger-yield': 'error',\n 'craft-ts/require-assert-exhaustive-route-exceptions': 'error',\n 'craft-ts/require-craft-exception-handler': 'error',\n 'craft-ts/require-exception-component-di-check': 'error',\n 'craft-ts/require-pending-component-di-check': 'error',\n 'craft-ts/require-child-route-mount-check': 'error',\n 'craft-ts/require-lazy-load-with-retry': 'error',\n 'craft-ts/require-cascade-route-di-check': 'error',\n 'craft-ts/global-exception-registry-match': 'error',\n },\n },\n];\n```\n\nWhat each rule does:\n\n- `craft-ts/prefer-craft-template-blocks`: keeps `craftComponent(...)` templates declarative by rejecting ternaries, logical expressions, negations, and imperative control flow; use `ifNode(...)`, `matchNode.exhaustive(...)`, `forNode(...)`, or `deferNode(...)`\n- `craft-ts/no-render-writes`: rejects detectable `set()`, `update()`, and `mutate()` calls in component templates and render bindings while allowing DOM event and `onXxx` output callbacks\n- `craft-ts/require-reactive-template-bindings`: requires signals, named Craft values, and component inputs to be read inside granular binding callbacks instead of during VNode construction; static values remain valid\n- `craft-ts/no-craft-use`: forbids the synchronous `craftUse(...)` escape hatch in Craft TypeScript files; use a generator and delegate the reader with `yield*` instead\n- `craft-ts/no-type-assertions-in-template`: forbids `as ...` and angle-bracket type assertions in Craft templates; fix the type in the logic factory or expose a correctly typed derived value\n- `craft-ts/no-ephemeral-template-form-state`: forbids `let` / `const` / `var` in the fourth argument of `craftComponent(...)` and `craftDirective(...)` (inline or a same-file identifier). Declare that state in the logic factory with `state()` or `craftComputed()` instead\n- `craft-ts/template-element-name-unique`: requires named HTML helpers to use a static, unique local name within a component; use the object-first helper form for unnamed elements such as `p({ id: 'hint' }, ...)`\n- `craft-ts/no-craft-computed-side-effects`: forbids writes and asynchronous work inside `craftComputed`; only reactive reads and `settled(...)` are allowed. The graph-wide counterpart is [`assertCraftComputedPure`](/guide/testing/architecture#assertcraftcomputedpure).\n- `craft-ts/no-effect-outside-loaders`: keeps `params`, methods, `craftComputed(...)`, and `craftEffect(...)` synchronous by allowing Effect values and Effect service reads only in Effect loaders; `no-effect-in-params` remains as a compatibility alias\n- `craft-ts/sync-effect-body`: keeps a body declared synchronous (`SyncOp` in its requirements) free of anything that may suspend — async constructors such as `Effect.sleep`/`Effect.promise`, and members nothing declares synchronous. Type-aware: the ESLint parser must use `projectService: true` or a TypeScript `project`\n- `craft-ts/no-explicit-effect-type`: lets `Effect.gen` infer its complete type instead of repeating an explicit Effect annotation; contracts declared in interfaces and type aliases remain allowed\n- `craft-ts/prefer-inline-effect-insertion`: keeps the `queryEffect` insertion factory inline so its resource and exception types are inferred without a separate `InsertionParams` context alias\n- `craft-ts/prefer-inline-route-providers`: inlines a route provider tuple used only once by `loadCraftComponent(...)`, preserving the route-level type proof\n- `craft-ts/prefer-craft-reactivity`: rejects authored signal/computed/effect/resource APIs, explicit `.subscribe()` calls, and RxJS `Subject`/`BehaviorSubject`/`ReplaySubject`; use `state`, `craftComputed`, `craftEffect`, `query`, and named `source$`/`on$` flows\n- `craft-ts/prefer-craft-service`: keeps services in the `craftService(...)` model\n- `craft-ts/no-craft-service-component-same-file`: forbids declaring `craftService(...)` and `craftComponent(...)` in the same file; a route-level service provider combined with a lazy-loaded component can break lazy loading, so keep them in separate files\n- `craft-ts/no-injection-token`: forbids authored `InjectionToken` contracts; declare them with `craftService({ name, providedIn: 'abstract' }, abstract<Contract>())`\n- `craft-ts/prefer-craft-http-client`: forbids direct transport usage in favor of `CraftHttpClient`\n- `craft-ts/prefer-craft-http-transport`: forbids direct `fetch()` and `XMLHttpRequest`; use `query()` for reads or `mutation()` for writes with `CraftHttpClient`\n- `craft-ts/prefer-craft-input-output`: keeps component inputs and outputs in the `Input`/`Output` model used by `craftComponent(...)`\n- `craft-ts/require-primitive-derived-property`: requires a `computed` or `craftComputed` that only depends on one primitive in the same component/service to be exposed by that primitive's insertion; simple cases are autofixed\n- `craft-ts/no-async-await`: forbids `async` functions, `await`, and `for await...of`; use generator-based Craft primitives, `craftSleep`, and `CraftHttpClient` instead\n- `craft-ts/no-throw`: forbids `throw` in Craft code and offers a Quick Fix that returns `craftException({ _tag: 'UNEXPECTED_ERROR' }, { error: ... })`; keep technical boundaries and tests outside this rule when their contracts require thrown errors\n- `craft-ts/no-imperative-craft-resource-trigger`: forbids `query.call(...)`, `mutation.mutate(...)`, and `asyncProcess.method(...)` in a `craftEffect` dependency graph, including through `craftGen(...)`. The graph-wide counterpart, including `state` / `source$` writes, is [`assertCraftEffectNoImperativeSync`](/guide/testing/architecture#assertcrafteffectnoimperativesync).\n- `craft-ts/no-imperative-craft-method-actions`: forbids composing multiple imperative actions in a `craftMethod`; emit a `source$` event and let the affected query react with `insertReactOnMutation(...)` instead. A handler such as `event.preventDefault()` followed by one `mutation.mutate(...)` remains valid.\n- `craft-ts/no-imperative-storage-in-craft-method`: forbids direct storage access and imperative location changes in a `craftMethod`; use `insertReactOnMutation(...)` with `optimisticUpdate: () => undefined` to clear the affected query and let its persistence follow the query state.\n- `craft-ts/no-transition-actions`: forbids `query.call(...)`, `mutation.mutate(...)`, and `asyncProcess.method(...)` inside `transitionStep(...)`; validate the event and emit a source, then let the resource react to that source.\n- `craft-ts/require-craft-resource-trigger-yield`: requires those triggers to use `yield*` inside generator functions, while ordinary UI callbacks may keep imperative calls\n- `craft-ts/require-craft-method-for-yieldable-callback`: requires callbacks returned by a `craftComponent` factory to wrap yieldable Craft method calls in `craftMethod(...)`\n- `craft-ts/prefer-direct-yieldable-callback`: replaces a template generator or generator method that only delegates `yield* callback()` with the callback reference itself (`callback` or `object.method`)\n- `craft-ts/require-yieldable-reactive-read`: requires Craft reactive readers to be delegated with `yield*` inside generator functions; a function that reads a Craft reader must itself be a generator\n- `craft-ts/require-yieldable-template-method`: requires yieldable Craft method calls in a `craftComponent` template to be delegated with `yield*`, or passed as a reference (`click: counter.increment`)\n- `craft-ts/require-yieldable-insertion-write`: requires `set(...)`, `patch(...)`, and `update(...)` to be delegated with `yield*` when they are used inside a generator method\n- `craft-ts/require-assert-exhaustive-route-exceptions`: adds the collection-level `assertExhaustiveRouteExceptions(...)` safety net\n- `craft-ts/require-craft-exception-handler`: enforces `craftExceptionHandler(function* (...) {})`; simple handlers are autofixed and ambiguous raw redirects are reported for manual migration\n- `craft-ts/require-exception-component-di-check`: generates O(1) `RouteExceptionComponentCheckedDI` checks for `renderComponent`, route-level `errorComponent`, `withErrorComponent`, `withRouteLoadError`, and route-local `provideRouteLoadErrorComponent`\n- `craft-ts/require-pending-component-di-check`: generates the independent `RouteCheckedDI` check for each `pendingComponent`\n- `craft-ts/no-raw-class`: forbids a `class:` binding that is a string, a template literal or a function, in any file that imports `@craft-ts/style`. A class assembled at render time is a visual state nothing recorded, so the [visual matrix](/guide/style/testing) would enumerate what the sheets declare while the DOM shows something else. Move the rule into the sheet and bind the class it returns; make the variation an axis and set a `data-*` attribute\n- `craft-ts/no-raw-css-value`: forbids a string or number literal as an argument to a `@craft-ts/style` helper — `p('12px')`, `bg('red')`. If the scale is missing the step, add it to the scale; if the value genuinely cannot be proven, `unsafeLength('13px', reason)` compiles and makes the debt countable in the [graph](/guide/style/testing#what-the-graph-adds)\n- `craft-ts/no-free-has`: forbids a hand-written `:has()` in styles. It reaches across the component boundary, so what a component looks like depends on markup it does not own — a state the matrix cannot enumerate. Use the `descendant` axis, which is a closed set and carries its own test driver\n- `craft-ts/style-file-boundary`: restricts a `*.style.ts` to style-vocabulary imports. The [build plugin](/guide/style/setup) imports the file in Node to read what it registered, so an application import would run application code at build time\n- `craft-ts/craft-css-token-registry`: reports a custom property registered with `@property` by two different components. A custom property may have only one owner; two silently fight over its syntax and initial value\n- `craft-ts/require-effect-adapters`: requires the Effect-aware adapters — `queryEffect`, `mutationEffect`, `asyncProcessEffect` — instead of the plain primitives in an Effect application. See [Choose the right adapter](/guide/advanced/effect#choose-the-right-adapter)\n- `craft-ts/craft-signal-source-name-match`: requires `signalSource(name, ...)` to take a string literal matching the variable, class property or object property it is assigned to, so the name in a trace is the name in the source. A computed name defeats the [architecture graph](/guide/testing/architecture), which reads these names statically\n- `craft-ts/require-child-route-mount-check`: adds the missing `assertChildRouteMounts(...)` call + import (Quick Fix) for any `craftRoutes(...)` collection that mounts lazy `loadChildren`, so a `.withParent`-pinned child mounted under the wrong path is a compile error\n- `craft-ts/require-lazy-load-with-retry`: wraps route `loadComponent` and `loadChildren` imports with the generated `withRetry(...)` loader helper while preserving a statically analyzable import specifier\n- `craft-ts/require-cascade-route-di-check`: rejects any `craftRoutes(...)` collection without a same-file `ValidateCascadeRoutesFile + CanRun` proof; its autofix adds the conservative `<never, Router>` context, which should be adjusted when the mount inherits providers\n- `craft-ts/global-exception-registry-match`: keeps `CraftGlobalExceptionRegistry` synchronized with handlers delegating to `globalError()`\n- `craft-ts/prefer-craft-router-link`: requires `CraftRouterLink` for internal `a(..., { href: ... })` navigation; external URLs, fragment links, downloads, `_blank`, and links marked with `data-navigation: 'external'` remain native\n\n### Accessibility (`craft-ts/a11y`)\n\nSpread `craftRules.configs.a11y.rules` to enable the WCAG 2.2 AA preset as\n`error`. The rules walk **all** hyperscript in the file (`craftTemplate`,\nextracted factories, `h('tag')`), not only `craftComponent` argument 3.\n\n- `prefer-named-html-helpers`: forbids `h('img')` / `h('button')` when a named helper exists\n- `require-interactive-local-name`: requires a string-literal first argument on interactive helpers; the local name is the third segment of `data-craft-name=\"${component}:${tag}:${localName}\"`\n- `img-has-alt`, `iframe-has-title`, `button-has-type`, `anchor-has-href`\n- `control-has-accessible-name`, `label-has-associated-control`, `heading-has-content`\n- `no-noninteractive-element-interactions`, `no-positive-tabindex`\n- `valid-aria`, `role-has-required-aria`, `target-blank-noopener`\n- `prefer-relative-heading`, `require-route-heading-outline`,\n `require-outlet-heading-section`, `no-heading-level-skip`\n- `require-focus-visible`, `require-reduced-motion` (CSS of `craftComponent`)\n\nSee [Accessibility](/guide/components/accessibility).\n\nThe two migration rules also expose a VS Code ESLint Quick Fix suggestion that inserts a temporary local disable comment with the intended migration note when you need to unblock a file before doing the full refactor.\n\nThe template and reactivity rules are intentionally diagnostic-only: replacing a\nresource or subscription can change lifecycle and error semantics, so the rule\npoints at the Craft primitive without applying a potentially unsafe rewrite.\n\n### Why templates use blocks\n\nCraft template blocks preserve the branch structure in the type-level render\ncontract. A ternary or `condition && node` produces only a computed value, so\nthe type checker cannot assert which branch renders which content. Keep derived\nvalues and business decisions in the component's state/query layer, then make\nthe template express visibility explicitly:\n\n```ts\nifNode(\n isReady,\n () => p('Ready'),\n () => p('Loading…'),\n);\n\nmatchNode.exhaustive(query.exceptions, '_tag', {\n NOT_FOUND: () => p('Not found'),\n FORBIDDEN: () => p('Forbidden'),\n});\n```\n\nThis rule is for Craft's TypeScript templates. It does not rewrite external\ntemplate languages.\n\nThe same restriction applies to boolean expressions. A negation is still\napplication logic, even when it is used only for a DOM property:\n\n```ts\n// Incorrect: the template derives the disabled state.\nbutton(\n {\n disabled: function* () {\n return !(yield* machine.canGoBack());\n },\n },\n 'Back',\n);\n\n// Correct: derive it in the logic factory and bind the result.\nconst backDisabled = craftComputed('backDisabled', function* () {\n return !(yield* history.canGoBack());\n});\nreturn { backDisabled };\n```\n\nKeep the template to layout and binding. Move labels, formatted values,\nvalidation state, and other decisions into `state()` or `craftComputed()`.\n\n### Derived values belong to their primitive\n\nWhen a computed reads only one local primitive, declare it in that primitive's\ninsertion. This keeps the dependency visible and lets pending/exception\nboundaries name the actual source:\n\n```ts\nconst users =\n yield *\n query('users', config, ({ resource }) => ({\n total: craftComputed('total', function* () {\n return (yield* settled(resource)).length;\n }),\n }));\n```\n\nDo not create `craftComputed('total', ...)` beside the query when the\ncomputation depends only on `users`.\n\n### Keep casts and synchronous reads out of templates\n\nCraft templates reject both `as ...` / angle-bracket assertions and\n`craftUse(...)`. Fix the type or perform the synchronous-to-reactive\nconversion in the component logic, then expose a typed reader or generator to\nthe template:\n\n```ts\nconst typedStep = machine.stepState as unknown as () => { step: Step };\nreturn { typedStep };\n\n// Template: no cast and no craftUse.\nmatchNode.exhaustive(typedStep, 'step', steps);\n```\n\n`no-craft-use` applies to Craft TypeScript files, not only the fourth\n`craftComponent(...)` argument. A synchronous integration boundary may opt out\nlocally when its external API cannot consume a generator, but application\nstate and templates should use `yield*`.\n\n### Form and accessibility diagnostics\n\nThe accessibility preset also checks the static structure of hyperscript:\n\n- give every `label` an `htmlFor` matching the control `id`, or wrap the control;\n- give named controls and helpers a unique string local name;\n- use `button` or `a` for interactions instead of adding `click` to a `div`;\n- add a `prefers-reduced-motion` branch whenever component CSS defines an\n animation or transition.\n\nThese checks run on Craft TypeScript templates and extracted helper factories,\nso moving markup into a local function does not bypass them.\n\n### Reactive values belong in binding callbacks\n\n`require-reactive-template-bindings` uses TypeScript type information to find\nreactive reads. Reading a signal while constructing a VNode would make it a\ndependency of the structural component render, so the rule rejects this form:\n\n```ts\n// Incorrect: count is read by the component template.\np(`Count: ${count()}`);\nbutton({ disabled: isDisabled() }, 'Save');\ndiv({ class: { active: isActive() } });\n```\n\nKeep each read inside the callback owned by its DOM binding. Pass a yieldable\nreader, or use a generator when the binding must format:\n\n```ts\np(count);\np(function* () {\n return `Count: ${yield* count()}`;\n});\nbutton({ disabled: isDisabled }, 'Save');\ndiv({ class: isActiveClass });\n```\n\nLiteral and otherwise static values are still allowed, as are reads performed\nfrom DOM events and `onXxx` output callbacks. Because the rule is type-aware,\nthe ESLint parser must use `projectService: true` or a TypeScript `project`.\n\n### Pass simple yieldable callbacks directly\n\n`prefer-direct-yieldable-callback` removes a generator wrapper when the\ntemplate only delegates one zero-argument callback. It handles both a value\nbinding and a generator method:\n\n```ts\n// Before: redundant wrappers around the callbacks.\nbutton(\n {\n *click() {\n yield* press();\n },\n },\n function* () {\n return yield* label();\n },\n);\n\n// After `eslint --fix`.\nbutton({ click: press }, label);\n```\n\nMember callbacks are supported as well when the access is static and has no\narguments:\n\n```ts\n// Before.\nspan(function* () {\n return yield* counter.increment();\n});\n\n// After.\nspan(counter.increment);\n```\n\nThe rule leaves callbacks with parameters, extra statements, or additional\ncomputation unchanged. In those cases the generator contains behavior that\ncannot be represented by passing the callback reference alone.\n\n### Yield insertion writes from generator methods\n\n`require-yieldable-insertion-write` requires `set(...)`, `patch(...)`, and\n`update(...)` calls to be delegated with `yield*` when they are used inside a\ngenerator method:\n\n```ts\nnextPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page + 1 });\n},\n```\n\nInsertion callbacks that are not generators may return a write directly; the\ninsertion wrapper consumes that result for them.\n\n## What generates what\n\nThree rules do more than complain — they write code you would otherwise\nmaintain by hand:\n\n| Rule | Generates |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `require-cascade-route-di-check` | the same-file DI proof for a `craftRoutes(...)` collection |\n| `require-assert-exhaustive-route-exceptions` | the collection-level exhaustiveness assert |\n| `require-child-route-mount-check` | the `assertChildRouteMounts(...)` call and its import |\n| `require-lazy-load-with-retry` | the `withRetry(...)` wrapper on lazy route imports |\n| `prefer-direct-yieldable-callback` | replaces redundant generators with direct callback references |\n\n## Adopting them progressively\n\nOn an existing codebase, enable them in waves rather than all at once:\n\n1. **The route safety nets** — the `require-*` rules. Mostly autofixable. They\n generate the proofs; [architecture tests](/guide/testing/architecture#assertroutediproofs)\n (`assertRouteDiProofs`) fail CI if a proof is later removed or left unarmed.\n2. **The architecture rules last** — `prefer-craft-service`,\n `no-craft-service-component-same-file`, `prefer-craft-http-client`,\n `require-yieldable-reactive-read`,\n `require-yieldable-template-method`, `require-yieldable-insertion-write`.\n These ask for real refactors.\n\nThe four style rules — `no-raw-class`, `no-raw-css-value`, `no-free-has`,\n`style-file-boundary` — are in `craftRules.configs.recommended` at `'error'`,\nand they are **gated on the import**: they fire only in files that import\n`@craft-ts/style`. A component you have not migrated is not claiming the\nguarantee, so nothing reports it. The day a file starts using the design system\nis the day it starts being held to it — which is why enabling them on an\nunmigrated codebase costs nothing.\n\nThe two migration rules also expose a VS Code quick fix that inserts a temporary\nlocal disable comment with the intended migration note, so you can unblock a\nfile before doing the full refactor.\n\n## See Also\n\n- [Routing setup](/guide/routing/setup) — where these rules are installed\n- [CLI automation](/guide/routing/automation) — the codemods they complement\n- [Architecture rules](/guide/testing/architecture) — graph-wide constraints ESLint cannot see\n- [Activating the style system](/guide/style/setup) — what the four style rules are guarding\n"
321
326
  },
322
327
  {
323
328
  "path": "/guide/routing/exception-handling",
@@ -427,7 +432,7 @@
427
432
  {
428
433
  "path": "/guide/state/url-state",
429
434
  "title": "queryParams",
430
- "body": "# queryParams\n\n`queryParams` is a state whose home is the URL's query string. Reading and\nwriting look like any other state; the address bar follows, and so does the back\nbutton.\n\n**Use it when** the value should survive a refresh and be shareable by copying\nthe link: filters, pagination, a selected tab.\n**Not when** the value is ephemeral or private — that's\n[`state`](/guide/state/local-state).\n\n::: tip No synchronisation code\nThere is no effect to write and no `ActivatedRoute` subscription. If you find\nyourself syncing a `state` with the URL, you want this primitive instead.\n:::\n\n## The common case\n\n```typescript\nimport { queryParams } from '@craft-ts/core';\n\nconst numberCodec = {\n decode: (value: string) => parseInt(value, 10),\n encode: (value: number) => String(value),\n};\nconst booleanCodec = {\n decode: (value: string) => value === 'true',\n encode: (value: boolean) => String(value),\n};\n\nconst pagination = yield* queryParams(\n 'pagination',\n {\n state: {\n page: { fallbackValue: 1, codec: numberCodec },\n showArchived: { fallbackValue: false, codec: booleanCodec },\n },\n },\n ({ set, update, patch, reset }) => ({ set, update, patch, reset }),\n);\n\npagination(); // { page: 1, showArchived: false }\npagination.page(); // 1\n\npagination.patch({ showArchived: true }); // navigates to ?showArchived=true\npagination.set({ page: 4, showArchived: false });\npagination.update((current) => ({ ...current, page: current.page + 1 }));\npagination.reset();\n```\n\n`?page=3&showArchived=true` becomes `{ page: 3, showArchived: true }` on load.\n\n## Codecs are mandatory\n\nA URL only holds strings, so every parameter declares how it converts both ways.\nThe decoded type is your application type; the encoded one is what appears in the\naddress bar.\n\n`fallbackValue` is what you get when the parameter is absent — which is why the\nstate type is never `undefined`.\n\nCodecs stay synchronous because they run inside the reactive URL computation.\n`@craft-ts/core` deliberately doesn't depend on a validation library: supply a\nsmall `{ decode, encode }` pair directly, or adapt one from the library you\nalready use.\n\n```typescript\n// arrays\ntags: {\n fallbackValue: [],\n codec: {\n decode: (value) => value.split(',').filter(Boolean),\n encode: (value) => value.join(','),\n },\n},\n\n// plain strings\nq: { fallbackValue: '', codec: { decode: String, encode: String } },\n```\n\nThe same pattern covers dates, enums and JSON-encoded objects.\n\n## Custom methods\n\n```typescript\nyield* queryParams(\n 'pagination',\n {\n state: { page: { fallbackValue: 1, codec: numberCodec } },\n },\n ({ state, patch }) => ({\n nextPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page + 1 });\n },\n previousPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page - 1 });\n },\n setPageSize: function* (pageSize: number) {\n return yield* patch({ pageSize, page: 1 });\n },\n }),\n);\n```\n\n## Feeding a query\n\nThe point of URL state is usually to drive a fetch. Read it from the query's\n`params`:\n\n```typescript\nyield* query('tasksQuery', {\n params: () => ({ page: pagination.page() }),\n loader: /* … */,\n});\n```\n\nOne direction of data flow: click → URL → loader → view.\n\n## Decode failures\n\nA `decode` that throws keeps the fallback value rather than corrupting your\nstate, and surfaces the failure:\n\n```typescript\nif (mode.hasException()) {\n mode.exceptions().list;\n mode.exceptions().parse.mode?.code; // 'QueryParamDecodeError'\n mode.exceptions().parse.mode?.payload;\n}\n```\n\nAn encode failure raises `QueryParamEncodeError` before router navigation starts.\n\n## Pitfalls\n\n**Every parameter needs a `codec`** — there is no implicit string passthrough.\n\n**Methods bound to a source with `on$` are not exposed** on the result, same as\nevery primitive.\n\n::: details Advanced — declaring query params on the route\nQuery parameters can live in the route rather than in a component, so they belong\nto the URL definition itself:\n\n```typescript\nexport const { demoRoutes, injectDemoQueryParamsQueryParams } = craftRoutes(\n 'demo',\n [\n {\n path: 'query-params',\n ...loadCraftComponent(({ withRetry }) =>\n withRetry(import('./qp-list-with-pagination')).then(\n ({ default: component }) => component,\n ),\n ),\n queryParams: function* () {\n const pagination = yield* queryParams(\n 'pagination',\n {\n state: {\n page: { fallbackValue: 1, codec: numberCodec },\n pageSize: { fallbackValue: 4, codec: numberCodec },\n },\n },\n ({ patch, state }) => ({\n nextPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page + 1 });\n },\n previousPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page - 1 });\n },\n updatePageSize: function* (pageSize: number) {\n return yield* patch({ pageSize, page: 1 });\n },\n }),\n );\n return pagination;\n },\n },\n ],\n);\n```\n\n\n\nWorking source:\n[exception-query-params.ts](https://github.com/craft-ts/craft-ts/blob/main/apps/demo/src/app/examples/primitives/exceptions/exception-query-params.ts).\n:::\n\n::: details Advanced — yielding dependencies\nThe insertion can be a generator, so a rule can come from a service:\n\n```typescript\nyield* queryParams(\n 'pagination',\n { state: { page: { fallbackValue: 1, codec: numberCodec } } },\n function* ({ patch, state }) {\n const maxPage = yield* PaginationRules.maxPage();\n return {\n nextPage: function* () {\n const current = yield* state();\n if (current.page >= maxPage()) return;\n return yield* patch(({ page }) => ({ page: page + 1 }));\n },\n };\n },\n);\n```\n\n:::\n\n::: tip Advanced — injectable writes\nInsertion methods provide `injectQueryParamsMethodRuntimeContext()`, and the\nURL state itself is published to `providePrimitiveResourceRuntimeObserver`.\nBoth expose `get`, `set`, `update`, and `patch` for wrappers, WebMCP tools,\nand other advanced patterns. See\n[Anatomy of a primitive](/guide/concepts/primitive-anatomy#injectable-runtime-context).\n:::\n\n## See Also\n\n- [Local state](/guide/state/local-state) — for non-URL state\n- [query](/guide/state/server-state) — consuming URL state from a loader\n- [Anatomy of a primitive](/guide/concepts/primitive-anatomy)\n"
435
+ "body": "# queryParams\n\n`queryParams` is a state whose home is the URL's query string. Reading and\nwriting look like any other state; the address bar follows, and so does the back\nbutton.\n\n**Use it when** the value should survive a refresh and be shareable by copying\nthe link: filters, pagination, a selected tab.\n**Not when** the value is ephemeral or private — that's\n[`state`](/guide/state/local-state).\n\n::: tip No synchronisation code\nThere is no effect to write and no `ActivatedRoute` subscription. If you find\nyourself syncing a `state` with the URL, you want this primitive instead.\n:::\n\n## The common case\n\n```typescript\nimport { queryParams } from '@craft-ts/core';\n\nconst numberCodec = {\n decode: (value: string) => parseInt(value, 10),\n encode: (value: number) => String(value),\n};\nconst booleanCodec = {\n decode: (value: string) => value === 'true',\n encode: (value: boolean) => String(value),\n};\n\nconst pagination =\n yield *\n queryParams(\n 'pagination',\n {\n state: {\n page: { fallbackValue: 1, codec: numberCodec },\n showArchived: { fallbackValue: false, codec: booleanCodec },\n },\n },\n ({ set, update, patch, reset }) => ({ set, update, patch, reset }),\n );\n\npagination(); // { page: 1, showArchived: false }\npagination.page(); // 1\n\npagination.patch({ showArchived: true }); // navigates to ?showArchived=true\npagination.set({ page: 4, showArchived: false });\npagination.update((current) => ({ ...current, page: current.page + 1 }));\npagination.reset();\n```\n\n`?page=3&showArchived=true` becomes `{ page: 3, showArchived: true }` on load.\n\n## Codecs are mandatory\n\nA URL only holds strings, so every parameter declares how it converts both ways.\nThe decoded type is your application type; the encoded one is what appears in the\naddress bar.\n\n`fallbackValue` is what you get when the parameter is absent — which is why the\nstate type is never `undefined`.\n\nCodecs stay synchronous because they run inside the reactive URL computation.\n`@craft-ts/core` deliberately doesn't depend on a validation library: supply a\nsmall `{ decode, encode }` pair directly, or adapt one from the library you\nalready use.\n\n```typescript\n// arrays\ntags: {\n fallbackValue: [],\n codec: {\n decode: (value) => value.split(',').filter(Boolean),\n encode: (value) => value.join(','),\n },\n},\n\n// plain strings\nq: { fallbackValue: '', codec: { decode: String, encode: String } },\n```\n\nThe same pattern covers dates, enums and JSON-encoded objects.\n\n## Effect Schema adapter\n\nWhen the application already uses Effect Schema, keep `queryParams` as the URL\nowner and adapt the schema's synchronous entry points. This keeps URL parsing\ntyped without introducing a second URL primitive:\n\n```typescript\nimport { Schema } from 'effect';\n\nconst Search = Schema.String;\nconst searchCodec = {\n decode: Schema.decodeUnknownSync(Search),\n encode: Schema.encodeSync(Search),\n};\n\nconst filters =\n yield *\n queryParams('filters', {\n state: {\n search: { fallbackValue: '', codec: searchCodec },\n },\n });\n```\n\nUse the same adapter shape for numbers, booleans, dates, enums and arrays. A\ntransformation schema is useful when the URL representation differs from the\nvalue used by the component; its `decode` and `encode` functions must still be\nsynchronous. Missing values use `fallbackValue`, malformed values keep that\nfallback and expose `QueryParamDecodeError`, and `reset()` removes the keys\nfrom the URL. This covers the common filter, sort and pagination cases while\npreserving stable serialization in one place.\n\nThere is deliberately no `queryParamsEffect`: URL synchronisation belongs to\nCraft, while an Effect query or loader reacts to the resulting typed state.\n\n## Custom methods\n\n```typescript\nyield *\n queryParams(\n 'pagination',\n {\n state: { page: { fallbackValue: 1, codec: numberCodec } },\n },\n ({ state, patch }) => ({\n nextPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page + 1 });\n },\n previousPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page - 1 });\n },\n setPageSize: function* (pageSize: number) {\n return yield* patch({ pageSize, page: 1 });\n },\n }),\n );\n```\n\n## Feeding a query\n\nThe point of URL state is usually to drive a fetch. Read it from the query's\n`params`:\n\n```typescript\nyield* query('tasksQuery', {\n params: () => ({ page: pagination.page() }),\n loader: /* … */,\n});\n```\n\nOne direction of data flow: click → URL → loader → view.\n\n## Decode failures\n\nA `decode` that throws keeps the fallback value rather than corrupting your\nstate, and surfaces the failure:\n\n```typescript\nif (mode.hasException()) {\n mode.exceptions().list;\n mode.exceptions().parse.mode?.code; // 'QueryParamDecodeError'\n mode.exceptions().parse.mode?.payload;\n}\n```\n\nAn encode failure raises `QueryParamEncodeError` before router navigation starts.\n\n## Pitfalls\n\n**Every parameter needs a `codec`** — there is no implicit string passthrough.\n\n**Methods bound to a source with `on$` are not exposed** on the result, same as\nevery primitive.\n\n::: details Advanced — declaring query params on the route\nQuery parameters can live in the route rather than in a component, so they belong\nto the URL definition itself:\n\n```typescript\nexport const { demoRoutes, injectDemoQueryParamsQueryParams } = craftRoutes(\n 'demo',\n [\n {\n path: 'query-params',\n ...loadCraftComponent(({ withRetry }) =>\n withRetry(import('./qp-list-with-pagination')).then(\n ({ default: component }) => component,\n ),\n ),\n queryParams: function* () {\n const pagination = yield* queryParams(\n 'pagination',\n {\n state: {\n page: { fallbackValue: 1, codec: numberCodec },\n pageSize: { fallbackValue: 4, codec: numberCodec },\n },\n },\n ({ patch, state }) => ({\n nextPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page + 1 });\n },\n previousPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page - 1 });\n },\n updatePageSize: function* (pageSize: number) {\n return yield* patch({ pageSize, page: 1 });\n },\n }),\n );\n return pagination;\n },\n },\n ],\n);\n```\n\nWorking source:\n[exception-query-params.ts](https://github.com/craft-ts/craft-ts/blob/main/apps/demo/src/app/examples/primitives/exceptions/exception-query-params.ts).\n:::\n\n::: details Advanced — yielding dependencies\nThe insertion can be a generator, so a rule can come from a service:\n\n```typescript\nyield *\n queryParams(\n 'pagination',\n { state: { page: { fallbackValue: 1, codec: numberCodec } } },\n function* ({ patch, state }) {\n const maxPage = yield* PaginationRules.maxPage();\n return {\n nextPage: function* () {\n const current = yield* state();\n if (current.page >= maxPage()) return;\n return yield* patch(({ page }) => ({ page: page + 1 }));\n },\n };\n },\n );\n```\n\n:::\n\n::: tip Advanced — injectable writes\nInsertion methods provide `injectQueryParamsMethodRuntimeContext()`, and the\nURL state itself is published to `providePrimitiveResourceRuntimeObserver`.\nBoth expose `get`, `set`, `update`, and `patch` for wrappers, WebMCP tools,\nand other advanced patterns. See\n[Anatomy of a primitive](/guide/concepts/primitive-anatomy#injectable-runtime-context).\n:::\n\n## See Also\n\n- [Local state](/guide/state/local-state) — for non-URL state\n- [query](/guide/state/server-state) — consuming URL state from a loader\n- [Anatomy of a primitive](/guide/concepts/primitive-anatomy)\n"
431
436
  },
432
437
  {
433
438
  "path": "/guide/style",
@@ -477,7 +482,7 @@
477
482
  {
478
483
  "path": "/guide/testing/architecture/craft-effect-imperative-sync",
479
484
  "title": "Keep `craftEffect` out of imperative synchronisation",
480
- "body": "# Keep `craftEffect` out of imperative synchronisation\n\n`assertCraftEffectNoImperativeSync` prevents a `craftEffect` from writing a\nstate/source or triggering another query, mutation or async process:\n\n\n\n## The syntax is valid — the placement is not\n\nThe following calls are valid Craft generator syntax. `set`, `call` and\n`mutate` return yieldable operations, so a generator consumes them with\n`yield*`:\n\n```typescript\nfunction* submit() {\n yield* searchResults.set(yield* rawResults());\n yield* usersQuery.call(yield* searchTerm());\n yield* saveMutation.mutate(yield* draft());\n}\n```\n\nThe problem is putting the same code in a `craftEffect`. This is exactly the\ncase rejected by `assertCraftEffectNoImperativeSync`:\n\n```typescript\ncraftEffect('sync', function* () {\n yield* searchResults.set(yield* rawResults());\n yield* usersQuery.call(yield* searchTerm());\n yield* saveMutation.mutate(yield* draft());\n});\n```\n\nThe rule is therefore not saying that `yield* searchResults.set(...)` is\ninvalid TypeScript or invalid Craft syntax. It is saying that a reactive\neffect must not imperatively write or trigger another Craft primitive.\n\n## What it prevents\n\nThis effect creates three hidden edges in the Craft graph:\n\n```typescript\ncraftEffect('sync', function* () {\n yield* searchResults.set(yield* rawResults());\n yield* usersQuery.call(yield* searchTerm());\n yield* saveMutation.mutate(yield* draft());\n});\n```\n\nThe graph is effectively:\n\n```text\nsync effect ──writes──▶ searchResults\n ├─calls────▶ usersQuery\n └─calls────▶ saveMutation\n```\n\nWhenever one of the values read by the effect changes, the effect can write\nstate, start a query and start a mutation again. The direction of data flow is\nhidden in a callback, which can create feedback loops, duplicate requests or a\nmutation that runs merely because a signal was read.\n\n## Use the primitive that owns the relationship instead\n\nIf the query depends on `searchTerm`, make that dependency explicit with\n`params`:\n\n```typescript\nconst usersQuery = yield* query('usersQuery', {\n params: searchTerm,\n loader: ({ params }) => searchUsers(params),\n});\n```\n\nIf all three operations belong to one explicit user action, use `craftMethod`\ninstead of `craftEffect`:\n\n```typescript\nconst sync = craftMethod('sync', function* () {\n yield* searchResults.set(yield* rawResults());\n yield* usersQuery.call(yield* searchTerm());\n yield* saveMutation.mutate(yield* draft());\n});\n```\n\nCall `sync` from the submit or click handler. It then runs once per explicit\ninvocation, rather than once per reactive recomputation.\n\nFor a mutation-to-query relationship, use an insertion such as\n`insertReactOnMutation`. For a named external event, use `on$`. Use a computed\nvalue when `searchResults` is only a transformation of `rawResults`, instead\nof storing a second value and synchronising it.\n\nLogging, focus and other effects that do not push into Craft primitives remain\nvalid. The rule protects synchronization, not all side effects.\n\n## See also\n\n- [Reacting to mutations](/guide/state/react-on-mutation)\n- [From event to source](/guide/reactivity/from-event-to-source)\n"
485
+ "body": "# Keep `craftEffect` out of imperative synchronisation\n\n`assertCraftEffectNoImperativeSync` prevents a `craftEffect` from writing a\nstate/source or triggering another query, mutation or async process:\n\n\n\n## The syntax is valid — the placement is not\n\nThe following calls are valid Craft generator syntax. `set`, `call` and\n`mutate` return yieldable operations, so a generator consumes them with\n`yield*`:\n\n```typescript\nfunction* submit() {\n yield* searchResults.set(yield* rawResults());\n yield* usersQuery.call(yield* searchTerm());\n yield* saveMutation.mutate(yield* draft());\n}\n```\n\nThe problem is putting the same code in a `craftEffect`. This is exactly the\ncase rejected by `assertCraftEffectNoImperativeSync`:\n\n```typescript\ncraftEffect('sync', function* () {\n yield* searchResults.set(yield* rawResults());\n yield* usersQuery.call(yield* searchTerm());\n yield* saveMutation.mutate(yield* draft());\n});\n```\n\nThe rule is therefore not saying that `yield* searchResults.set(...)` is\ninvalid TypeScript or invalid Craft syntax. It is saying that a reactive\neffect must not imperatively write or trigger another Craft primitive.\n\n## What it prevents\n\nThis effect creates three hidden edges in the Craft graph:\n\n```typescript\ncraftEffect('sync', function* () {\n yield* searchResults.set(yield* rawResults());\n yield* usersQuery.call(yield* searchTerm());\n yield* saveMutation.mutate(yield* draft());\n});\n```\n\nThe graph is effectively:\n\n```text\nsync effect ──writes──▶ searchResults\n ├─calls────▶ usersQuery\n └─calls────▶ saveMutation\n```\n\nWhenever one of the values read by the effect changes, the effect can write\nstate, start a query and start a mutation again. The direction of data flow is\nhidden in a callback, which can create feedback loops, duplicate requests or a\nmutation that runs merely because a signal was read.\n\n## Use the primitive that owns the relationship instead\n\nIf the query depends on `searchTerm`, make that dependency explicit with\n`params`:\n\n```typescript\nconst usersQuery =\n yield *\n query('usersQuery', {\n params: searchTerm,\n loader: ({ params }) => searchUsers(params),\n });\n```\n\nIf the operation is a single explicit user action, a `craftMethod` may call one\nmutation after normalising the event:\n\n```typescript\nconst save = craftMethod('save', function* (event: Event) {\n event.preventDefault();\n yield* saveMutation.mutate(yield* draft());\n});\n```\n\nFor several operations belonging to one event, emit a `source$` directly from\nthe submit or click handler and let each affected primitive react to it. A\nmutation-to-query relationship belongs in `insertReactOnMutation`, not beside\nthe mutation call site:\n\n```typescript\nconst signOut$ = source$<void>('signOut$');\n\nbutton({ click: () => signOut$.emit() }, 'Sign out');\n\nconst logout =\n yield *\n mutation('logout', {\n method: signOut$.asReadonly(),\n loader: logoutUser,\n });\n\nconst session =\n yield *\n query(\n 'session',\n { params: () => 'current', loader: loadSession },\n insertReactOnMutation(logout, {\n optimisticUpdate: () => undefined,\n }),\n );\n```\n\n`craft-ts/no-imperative-craft-method-actions` and\n`craft-ts/no-imperative-storage-in-craft-method` enforce this placement in the\neditor. Storage adapters and intentionally imperative facades remain valid in\ntheir `craftService` seam.\n\nFor a mutation-to-query relationship, use an insertion such as\n`insertReactOnMutation`. For a named external event, use `on$`. Use a computed\nvalue when `searchResults` is only a transformation of `rawResults`, instead\nof storing a second value and synchronising it.\n\nLogging, focus and other effects that do not push into Craft primitives remain\nvalid. The rule protects synchronization, not all side effects.\n\n## See also\n\n- [Reacting to mutations](/guide/state/react-on-mutation)\n- [From event to source](/guide/reactivity/from-event-to-source)\n"
481
486
  },
482
487
  {
483
488
  "path": "/guide/testing/architecture/craft-effect-network",
@@ -642,7 +647,7 @@
642
647
  {
643
648
  "path": "/learn-effect/09-server-functions",
644
649
  "title": "9. Call server functions — proof of concept",
645
- "body": "# 9. Call server functions — proof of concept\n\n::: danger Not a final contract\n\nThe server-function integration is currently a **proof of concept**. The file\nconventions, transport, middleware composition and production integration are\nnot definitive yet and may change. Use this chapter to understand the current\nexperiment and to build demos; do not treat it as a stable deployment API.\n\n:::\n\n**Goal:** understand the current client → registry → Effect server path.\n\n## The current shape\n\nAn exposed function has a server implementation and a client facade:\n\n```text\nusers/list.fn-client.ts\nusers/list.fn-serveur.ts\n │\n └── HTTP/RPC → createServer registry → Effect handler\n```\n\nThe client imports only the server function's type. It must not import the\nserver implementation at runtime.\n\n## Define the server implementation\n\nThe current experimental API takes an identifier, an input schema and an\nexposure mode. The handler returns an Effect:\n\n```typescript\n// users/list.fn-serveur.ts\nimport { serverFunction } from '@craft-ts/core';\nimport { Effect, Schema } from 'effect';\n\nconst inputSchema = Schema.toStandardSchemaV1(\n Schema.Struct({ filter: Schema.String }),\n);\n\nexport const listUsers = serverFunction(\n 'demo.users.list',\n inputSchema,\n { exposure: 'client' },\n).handler(({ input }) =>\n Effect.gen(function* () {\n const repository = yield* UserRepository;\n return yield* repository.list(input.filter);\n }),\n);\n```\n\nThe server handler's success, typed errors and Effect requirements are the source\nof truth. Do not duplicate a result type or an error list manually.\n\n## Define the client facade\n\n```typescript\n// users/list.fn-client.ts\nimport { createServerFunctionClient } from '@craft-ts/core';\nimport type { listUsers as ServerListUsers } from './list.fn-serveur';\n\nexport const getUsers = createServerFunctionClient<typeof ServerListUsers>(\n 'demo.users.list',\n);\n```\n\nThe component uses the facade like a typed function. Wrap it in `queryEffect` or\n`mutationEffect` if the call belongs to a resource lifecycle:\n\n```typescript\nimport { isCraftException } from '@craft-ts/core';\n\nconst users = yield* queryEffect('users', {\n params: () => ({ filter: search() }),\n loader: ({ params }) =>\n Effect.gen(function* () {\n const result = yield* Effect.promise(() => getUsers(params));\n if (isCraftException(result)) return yield* Effect.fail(result);\n return result;\n }),\n});\n```\n\nThe exact transport adapter is still experimental. The repository's\n`demo-with-server-function` app currently uses `createServer`, `executeEffect`\nand a local HTTP bridge.\n\n## Register and execute on the server\n\n```typescript\nconst application = createServer({\n functions: [listUsers],\n execute: executeEffect(runtimeLayer).run,\n});\n```\n\nThe runtime Layer supplies server-only services such as a repository or the\ncurrent user. Never import secrets, credentials or server implementations into\na client module.\n\n## Middleware and security\n\nThe current demo also shows Effect middleware:\n\n```typescript\nconst audited = effectServerMiddleware('demo.audit', ({ next }) =>\n Effect.gen(function* () {\n yield* Effect.log('before');\n const result = yield* Effect.exit(next());\n yield* Effect.log('after');\n return yield* result;\n }),\n);\n```\n\nMiddleware may add typed failures, resolve Effect services and run before/after\nhooks. Client claims remain untrusted; authenticate and authorize again on the\nserver, then publish only verified values to the handler context.\n\n## Current limitations\n\nTreat these as constraints of the POC, not promises of the final design:\n\n- the browser transport and development plugin are local experimental adapters;\n- client/server file boundaries are checked by the current architecture graph,\n but deployment integration is still evolving;\n- middleware APIs and the server registry may be renamed or reshaped;\n- the server must re-check authorization even if the client has a matching\n Effect Layer.\n\nSee the running examples in\n[`apps/demo-with-server-function`](https://github.com/craft-ts/craft-ts/tree/main/apps/demo-with-server-function)\nand the server-function architecture plan in the repository when this work is\npromoted out of the prototype area.\n\n## What you gained\n\nYou can experiment with typed Effect server calls while keeping an explicit\nclient/server boundary. Keep this chapter isolated from stable application\ncontracts until the POC is replaced by a final server-function API.\n\n<div style=\"display: flex; justify-content: space-between; margin-top: 2rem\">\n\n[← 8. Test the graph](/learn-effect/08-testing)\n\n[Back to the overview →](/learn-effect/)\n\n</div>\n"
650
+ "body": "# 9. Call server functions — proof of concept\n\n::: danger Not a final contract\n\nThe server-function integration is currently a **proof of concept**. The file\nconventions, transport, middleware composition and production integration are\nnot definitive yet and may change. Use this chapter to understand the current\nexperiment and to build demos; do not treat it as a stable deployment API.\n\n:::\n\nFor the recommended public/protected access path, start with the\n[server functions guide](/guide/app/server-functions). This chapter keeps the\nlower-level POC details; the guide highlights the existing middleware,\n`clientContext`, session verification and refusal tests.\n\n**Goal:** understand the current client → registry → Effect server path.\n\n## The current shape\n\nAn exposed function has a server implementation and a client facade:\n\n```text\nusers/list.fn-client.ts\nusers/list.fn-serveur.ts\n │\n └── HTTP/RPC → createServer registry → Effect handler\n```\n\nThe client imports only the server function's type. It must not import the\nserver implementation at runtime.\n\n## Define the server implementation\n\nThe current experimental API takes an identifier, an input schema and an\nexposure mode. The handler returns an Effect:\n\n```typescript\n// users/list.fn-serveur.ts\nimport { serverFunction } from '@craft-ts/core';\nimport { Effect, Schema } from 'effect';\n\nconst inputSchema = Schema.toStandardSchemaV1(\n Schema.Struct({ filter: Schema.String }),\n);\n\nexport const listUsers = serverFunction('demo.users.list', inputSchema, {\n exposure: 'client',\n}).handler(({ input }) =>\n Effect.gen(function* () {\n const repository = yield* UserRepository;\n return yield* repository.list(input.filter);\n }),\n);\n```\n\nThe server handler's success, typed errors and Effect requirements are the source\nof truth. Do not duplicate a result type or an error list manually.\n\n## Define the client facade\n\n```typescript\n// users/list.fn-client.ts\nimport { createServerFunctionClient } from '@craft-ts/core';\nimport type { listUsers as ServerListUsers } from './list.fn-serveur';\n\nexport const getUsers =\n createServerFunctionClient<typeof ServerListUsers>('demo.users.list');\n```\n\nThe component uses the facade like a typed function. Wrap it in `queryEffect` or\n`mutationEffect` if the call belongs to a resource lifecycle:\n\n```typescript\nimport { isCraftException } from '@craft-ts/core';\n\nconst users =\n yield *\n queryEffect('users', {\n params: () => ({ filter: search() }),\n loader: ({ params }) =>\n Effect.gen(function* () {\n const result = yield* Effect.promise(() => getUsers(params));\n if (isCraftException(result)) return yield* Effect.fail(result);\n return result;\n }),\n });\n```\n\nThe exact transport adapter is still experimental. The repository's\n`demo-with-server-function` app currently uses `createServer`, `executeEffect`\nand a local HTTP bridge.\n\n### Transport failures are typed\n\nThe client transport always converts failures that prevent a usable server\nfunction response into a `CraftException` with `_tag: 'HttpError'` and\n`scope: 'ServerFunctionClient'`. This includes a lost connection, an aborted\nrequest, a missing `fetch` implementation, a rejected custom transport and an\nunreadable response body. As with `CraftHttpClient`, a network failure has\n`payload.status === 0`; the original failure is kept in `payload.body`.\n\nServer-side business failures remain their declared tags, so connection loss\nand domain errors can be handled separately by the same resource or mutation.\n\n## Register and execute on the server\n\n```typescript\nconst application = createServer({\n functions: [listUsers],\n execute: executeEffect(runtimeLayer).run,\n});\n```\n\nThe runtime Layer supplies server-only services such as a repository or the\ncurrent user. Never import secrets, credentials or server implementations into\na client module.\n\n## Middleware and security\n\nThe current demo also shows Effect middleware:\n\n```typescript\nconst audited = effectServerMiddleware('demo.audit', ({ next }) =>\n Effect.gen(function* () {\n yield* Effect.log('before');\n const result = yield* Effect.exit(next());\n yield* Effect.log('after');\n return yield* result;\n }),\n);\n```\n\nMiddleware may add typed failures, resolve Effect services and run before/after\nhooks. Client claims remain untrusted; authenticate and authorize again on the\nserver, then publish only verified values to the handler context.\n\n## Current limitations\n\nTreat these as constraints of the POC, not promises of the final design:\n\n- the browser transport and development plugin are local experimental adapters;\n- client/server file boundaries are checked by the current architecture graph,\n but deployment integration is still evolving;\n- middleware APIs and the server registry may be renamed or reshaped;\n- the server must re-check authorization even if the client has a matching\n Effect Layer.\n\nSee the running examples in\n[`apps/demo-with-server-function`](https://github.com/craft-ts/craft-ts/tree/main/apps/demo-with-server-function)\nand the server-function architecture plan in the repository when this work is\npromoted out of the prototype area.\n\n## What you gained\n\nYou can experiment with typed Effect server calls while keeping an explicit\nclient/server boundary. Keep this chapter isolated from stable application\ncontracts until the POC is replaced by a final server-function API.\n\n<div style=\"display: flex; justify-content: space-between; margin-top: 2rem\">\n\n[← 8. Test the graph](/learn-effect/08-testing)\n\n[Back to the overview →](/learn-effect/)\n\n</div>\n"
646
651
  },
647
652
  {
648
653
  "path": "/learn/01-first-state",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-ts/mcp",
3
- "version": "0.7.0-beta.17",
3
+ "version": "0.7.0-beta.18",
4
4
  "description": "MCP server, Agent Skills, and LLM files for coding agents using @craft-ts/core",
5
5
  "author": "Romain Geffrault",
6
6
  "license": "MIT",
@@ -63,6 +63,13 @@ If MCP is not configured, read https://ng-angular-stack.github.io/craft/llms.txt
63
63
  is a compile error. `@craft-ts/i18n` has no framework and no Effect import;
64
64
  use `@craft-ts/i18n-effect` only inside an Effect program. Load
65
65
  `craft-ts-i18n` before adding a key, a locale or a token.
66
+ - Forms start with `state` + `insertForm`. Choose `insertSelectFormTree` for a
67
+ nested field, `insertFormAttributes` for validators/visibility,
68
+ `insertFormSchema` for whole-form validation and `insertFormSubmit` for a
69
+ mutation. Bind the selected field with `CraftFieldDirective`; expose
70
+ `field.exceptions` through `fieldErrorNode`, and read submission state with
71
+ `form().submitting()` / `form().hasSubmitExceptions()`. Do not reach for
72
+ native `FormData` as the primary form model.
66
73
  - Run existing architecture tests. Do not add an architecture rule for the feature.
67
74
  - Keep `npm run typecheck` in the project CI; for generated projects this is
68
75
  already wired into `.github/workflows/ci.yml` alongside the architecture