@warlock.js/core 5.34.3 → 5.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/esm/cli/cli-commands.manager.mjs +1 -1
  3. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  4. package/esm/cli/commands/add.command.mjs +1 -0
  5. package/esm/cli/commands/add.command.mjs.map +1 -1
  6. package/esm/cli/commands/build.command.mjs +30 -3
  7. package/esm/cli/commands/build.command.mjs.map +1 -1
  8. package/esm/cli/commands/check/check-command.action.mjs +1 -0
  9. package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
  10. package/esm/cli/commands/check.command.mjs +5 -0
  11. package/esm/cli/commands/check.command.mjs.map +1 -1
  12. package/esm/cli/commands/explain/explain-command.action.mjs +21 -0
  13. package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -1
  14. package/esm/cli/commands/explain.command.mjs +14 -4
  15. package/esm/cli/commands/explain.command.mjs.map +1 -1
  16. package/esm/cli/commands/generate/generate.command.mjs +19 -1
  17. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  18. package/esm/cli/commands/generate/generators/endpoint.generator.mjs +1 -1
  19. package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -1
  20. package/esm/cli/commands/generate/generators/web.generator.mjs +169 -0
  21. package/esm/cli/commands/generate/generators/web.generator.mjs.map +1 -0
  22. package/esm/cli/commands/generate/templates/web-stubs.mjs +231 -0
  23. package/esm/cli/commands/generate/templates/web-stubs.mjs.map +1 -0
  24. package/esm/cli/commands/inspect/inspect-command.action.mjs +4 -3
  25. package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -1
  26. package/esm/cli/commands/inspect.command.mjs +13 -8
  27. package/esm/cli/commands/inspect.command.mjs.map +1 -1
  28. package/esm/cli/commands/mcp/mcp-command.action.mjs +1 -1
  29. package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -1
  30. package/esm/config/config-loader.d.mts.map +1 -1
  31. package/esm/config/config-loader.mjs +2 -0
  32. package/esm/config/config-loader.mjs.map +1 -1
  33. package/esm/config/load-config-files.mjs +4 -0
  34. package/esm/config/load-config-files.mjs.map +1 -1
  35. package/esm/dev-server/file-event-handler.mjs +11 -1
  36. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  37. package/esm/dev-server/file-manager.d.mts +1 -1
  38. package/esm/dev-server/file-manager.d.mts.map +1 -1
  39. package/esm/dev-server/file-manager.mjs.map +1 -1
  40. package/esm/dev-server/files-orchestrator.mjs +16 -1
  41. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  42. package/esm/dev-server/files-watcher.mjs +13 -4
  43. package/esm/dev-server/files-watcher.mjs.map +1 -1
  44. package/esm/dev-server/loader/loader-port-acks.mjs +21 -9
  45. package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
  46. package/esm/dev-server/loader/register-loader.mjs +31 -12
  47. package/esm/dev-server/loader/register-loader.mjs.map +1 -1
  48. package/esm/dev-server/module-loader.mjs +14 -0
  49. package/esm/dev-server/module-loader.mjs.map +1 -1
  50. package/esm/dev-server/special-file-patterns.mjs +4 -1
  51. package/esm/dev-server/special-file-patterns.mjs.map +1 -1
  52. package/esm/e2e/index.d.mts +2 -2
  53. package/esm/e2e/page.d.mts +6 -1
  54. package/esm/e2e/page.d.mts.map +1 -1
  55. package/esm/e2e/page.mjs +36 -1
  56. package/esm/e2e/page.mjs.map +1 -1
  57. package/esm/e2e/types.d.mts +31 -0
  58. package/esm/e2e/types.d.mts.map +1 -1
  59. package/esm/e2e/types.mjs.map +1 -1
  60. package/esm/errors/error-codes.d.mts +48 -0
  61. package/esm/errors/error-codes.d.mts.map +1 -1
  62. package/esm/errors/error-codes.mjs +48 -0
  63. package/esm/errors/error-codes.mjs.map +1 -1
  64. package/esm/errors/index.d.mts +2 -2
  65. package/esm/errors/index.mjs +2 -2
  66. package/esm/generations/add-command.action.mjs +2 -0
  67. package/esm/generations/add-command.action.mjs.map +1 -1
  68. package/esm/generations/design-system-payload.mjs +53 -0
  69. package/esm/generations/design-system-payload.mjs.map +1 -0
  70. package/esm/generations/features/design-system.feature.mjs +38 -0
  71. package/esm/generations/features/design-system.feature.mjs.map +1 -0
  72. package/esm/generations/features/index.mjs +3 -1
  73. package/esm/generations/features/index.mjs.map +1 -1
  74. package/esm/generations/features/notifications.feature.mjs +13 -2
  75. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  76. package/esm/generations/features/test.feature.mjs +48 -13
  77. package/esm/generations/features/test.feature.mjs.map +1 -1
  78. package/esm/generations/stubs.mjs +9 -5
  79. package/esm/generations/stubs.mjs.map +1 -1
  80. package/esm/http/error-mapper.d.mts +1 -1
  81. package/esm/http/error-mapper.mjs +3 -3
  82. package/esm/http/error-mapper.mjs.map +1 -1
  83. package/esm/http/error-response.d.mts +3 -0
  84. package/esm/http/error-response.d.mts.map +1 -1
  85. package/esm/http/error-response.mjs +8 -2
  86. package/esm/http/error-response.mjs.map +1 -1
  87. package/esm/http/errors/errors.d.mts +8 -0
  88. package/esm/http/errors/errors.d.mts.map +1 -1
  89. package/esm/http/errors/errors.mjs +13 -1
  90. package/esm/http/errors/errors.mjs.map +1 -1
  91. package/esm/http/index.d.mts +3 -2
  92. package/esm/http/index.mjs +3 -2
  93. package/esm/http/list-endpoint.d.mts +35 -0
  94. package/esm/http/list-endpoint.d.mts.map +1 -0
  95. package/esm/http/list-endpoint.mjs +28 -0
  96. package/esm/http/list-endpoint.mjs.map +1 -0
  97. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  98. package/esm/http/middleware/inject-request-context.mjs +12 -2
  99. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  100. package/esm/http/server.mjs +1 -1
  101. package/esm/index.d.mts +8 -6
  102. package/esm/index.mjs +3 -2
  103. package/esm/openapi/security.mjs +12 -4
  104. package/esm/openapi/security.mjs.map +1 -1
  105. package/esm/openapi/types.d.mts +2 -0
  106. package/esm/openapi/types.d.mts.map +1 -1
  107. package/esm/production/build-budgets.d.mts +46 -0
  108. package/esm/production/build-budgets.d.mts.map +1 -0
  109. package/esm/production/build-budgets.mjs +109 -0
  110. package/esm/production/build-budgets.mjs.map +1 -0
  111. package/esm/production/production-builder.mjs +13 -1
  112. package/esm/production/production-builder.mjs.map +1 -1
  113. package/esm/production/resolve-build-config.d.mts +1 -1
  114. package/esm/production/resolve-build-config.d.mts.map +1 -1
  115. package/esm/production/resolve-build-config.mjs.map +1 -1
  116. package/esm/restful/restful.d.mts +1 -1
  117. package/esm/tests/fakes/storage-fake.d.mts +3 -1
  118. package/esm/tests/fakes/storage-fake.d.mts.map +1 -1
  119. package/esm/tests/fakes/storage-fake.mjs +3 -3
  120. package/esm/tests/fakes/storage-fake.mjs.map +1 -1
  121. package/esm/tests/fakes/test-fakes.d.mts +3 -1
  122. package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
  123. package/esm/tests/fakes/test-fakes.mjs +3 -2
  124. package/esm/tests/fakes/test-fakes.mjs.map +1 -1
  125. package/esm/tests/index.d.mts +4 -2
  126. package/esm/tests/index.mjs +3 -1
  127. package/esm/tests/load-unit-config.mjs +24 -0
  128. package/esm/tests/load-unit-config.mjs.map +1 -0
  129. package/esm/tests/response-contract.d.mts +32 -0
  130. package/esm/tests/response-contract.d.mts.map +1 -0
  131. package/esm/tests/response-contract.mjs +116 -0
  132. package/esm/tests/response-contract.mjs.map +1 -0
  133. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  134. package/esm/tests/start-http-development-server.mjs +10 -2
  135. package/esm/tests/start-http-development-server.mjs.map +1 -1
  136. package/esm/tests/test-lifecycle-state.mjs.map +1 -1
  137. package/esm/tests/test-locales.mjs +33 -0
  138. package/esm/tests/test-locales.mjs.map +1 -0
  139. package/esm/tests/test-runtime-scope.d.mts +15 -0
  140. package/esm/tests/test-runtime-scope.d.mts.map +1 -0
  141. package/esm/tests/test-runtime-scope.mjs +80 -0
  142. package/esm/tests/test-runtime-scope.mjs.map +1 -0
  143. package/esm/tests/vitest-setup.d.mts +13 -3
  144. package/esm/tests/vitest-setup.d.mts.map +1 -1
  145. package/esm/tests/vitest-setup.mjs +146 -18
  146. package/esm/tests/vitest-setup.mjs.map +1 -1
  147. package/esm/tooling/check/affected-files.d.mts +5 -0
  148. package/esm/tooling/check/affected-files.d.mts.map +1 -0
  149. package/esm/tooling/check/affected-files.mjs +56 -0
  150. package/esm/tooling/check/affected-files.mjs.map +1 -0
  151. package/esm/tooling/check/changed-files.d.mts +2 -0
  152. package/esm/tooling/check/changed-files.d.mts.map +1 -1
  153. package/esm/tooling/check/changed-files.mjs +2 -2
  154. package/esm/tooling/check/changed-files.mjs.map +1 -1
  155. package/esm/tooling/check/conventions-step.d.mts +2 -1
  156. package/esm/tooling/check/conventions-step.d.mts.map +1 -1
  157. package/esm/tooling/check/conventions-step.mjs +48 -6
  158. package/esm/tooling/check/conventions-step.mjs.map +1 -1
  159. package/esm/tooling/check/fixes.d.mts +8 -0
  160. package/esm/tooling/check/fixes.d.mts.map +1 -0
  161. package/esm/tooling/check/fixes.mjs +31 -0
  162. package/esm/tooling/check/fixes.mjs.map +1 -0
  163. package/esm/tooling/check/index.d.mts +12 -0
  164. package/esm/tooling/check/index.mjs +3 -2
  165. package/esm/tooling/check/run-check.d.mts +2 -0
  166. package/esm/tooling/check/run-check.d.mts.map +1 -1
  167. package/esm/tooling/check/run-check.mjs +20 -4
  168. package/esm/tooling/check/run-check.mjs.map +1 -1
  169. package/esm/tooling/check/run-tests.mjs +1 -1
  170. package/esm/tooling/check/source-ast.d.mts +1 -0
  171. package/esm/tooling/check/web-conventions.d.mts +10 -0
  172. package/esm/tooling/check/web-conventions.d.mts.map +1 -0
  173. package/esm/tooling/check/web-conventions.mjs +126 -0
  174. package/esm/tooling/check/web-conventions.mjs.map +1 -0
  175. package/esm/tooling/index.d.mts +7 -2
  176. package/esm/tooling/index.mjs +4 -2
  177. package/esm/tooling/inspect/collect-files.mjs +172 -0
  178. package/esm/tooling/inspect/collect-files.mjs.map +1 -0
  179. package/esm/tooling/inspect/collect-pages.mjs +3 -2
  180. package/esm/tooling/inspect/collect-pages.mjs.map +1 -1
  181. package/esm/tooling/inspect/collect-routes.mjs +1 -1
  182. package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
  183. package/esm/tooling/inspect/explain-file.d.mts +12 -0
  184. package/esm/tooling/inspect/explain-file.d.mts.map +1 -0
  185. package/esm/tooling/inspect/explain-file.mjs +45 -0
  186. package/esm/tooling/inspect/explain-file.mjs.map +1 -0
  187. package/esm/tooling/inspect/index.d.mts +3 -2
  188. package/esm/tooling/inspect/index.mjs +2 -1
  189. package/esm/tooling/inspect/inspect-app.d.mts.map +1 -1
  190. package/esm/tooling/inspect/inspect-app.mjs +9 -3
  191. package/esm/tooling/inspect/inspect-app.mjs.map +1 -1
  192. package/esm/tooling/inspect/inspect-report.schema.mjs +34 -1
  193. package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -1
  194. package/esm/tooling/inspect/inspect-report.types.d.mts +17 -1
  195. package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -1
  196. package/esm/tooling/inspect/inspect-report.types.mjs +2 -1
  197. package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -1
  198. package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -1
  199. package/esm/tooling/mcp/mcp-tools.mjs +8 -2
  200. package/esm/tooling/mcp/mcp-tools.mjs.map +1 -1
  201. package/esm/tooling/problem.d.mts +4 -0
  202. package/esm/tooling/problem.d.mts.map +1 -1
  203. package/esm/tooling/problem.mjs +1 -0
  204. package/esm/tooling/problem.mjs.map +1 -1
  205. package/esm/tooling/syntax/ast.d.mts +2 -0
  206. package/esm/tooling/typecheck/run-typecheck.mjs +5 -1
  207. package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -1
  208. package/esm/vite/index.d.mts +3 -1
  209. package/esm/vite/index.mjs +3 -1
  210. package/esm/vite/test-setup-reporter.d.mts +16 -0
  211. package/esm/vite/test-setup-reporter.d.mts.map +1 -0
  212. package/esm/vite/test-setup-reporter.mjs +55 -0
  213. package/esm/vite/test-setup-reporter.mjs.map +1 -0
  214. package/esm/vite/test-workers.d.mts +5 -0
  215. package/esm/vite/test-workers.d.mts.map +1 -0
  216. package/esm/vite/test-workers.mjs +38 -0
  217. package/esm/vite/test-workers.mjs.map +1 -0
  218. package/esm/warlock-config/index.d.mts +2 -1
  219. package/esm/warlock-config/types.d.mts +3 -0
  220. package/esm/warlock-config/types.d.mts.map +1 -1
  221. package/llms-full.txt +763 -9
  222. package/llms.txt +2 -0
  223. package/package.json +15 -14
  224. package/schemas/inspect-report.schema.json +47 -1
  225. package/skills/build-budgets/SKILL.md +51 -0
  226. package/skills/build-restful/SKILL.md +56 -0
  227. package/skills/configure-app/SKILL.md +7 -0
  228. package/skills/create-module/SKILL.md +9 -0
  229. package/skills/generate-web-ui/SKILL.md +71 -0
  230. package/skills/index.md +2 -2
  231. package/skills/run-app/SKILL.md +1 -1
  232. package/skills/send-mail/SKILL.md +25 -0
  233. package/skills/send-response/SKILL.md +50 -0
  234. package/skills/test-browser/SKILL.md +53 -0
  235. package/skills/test-http/SKILL.md +91 -0
  236. package/skills/test-service/SKILL.md +161 -3
  237. package/skills/upgrade-from-5-29/SKILL.md +18 -0
  238. package/skills/use-localization/SKILL.md +21 -0
  239. package/skills/warlock-check/SKILL.md +72 -1
  240. package/skills/warlock-inspect/SKILL.md +45 -2
  241. package/skills/warlock-mcp/SKILL.md +4 -0
  242. package/skills/write-cli-command/SKILL.md +18 -0
  243. package/esm/production/build-app-production.mjs +0 -14
  244. package/esm/production/build-app-production.mjs.map +0 -1
package/llms.txt CHANGED
@@ -9,6 +9,7 @@
9
9
  - [Package orientation](@warlock.js/core/skills/index.md): Warlock application runtime: `router`, `Request`, `Response`, `config`, `env`, `sendMail` and `defineConfig`. Use for "add an API route", controllers, middleware, CLI commands, config, uploads, mail, sockets, OpenAPI, testing or upgrades. Not this package → @warlock.js/web for React SSR pages; @warlock.js/cascade for database queries; @warlock.js/seal for standalone schemas; @warlock.js/auth for login.
10
10
  - [add-connector](@warlock.js/core/skills/add-connector/SKILL.md): Extend Warlock with a `BaseConnector` subclass and register its instance in `warlock.config.ts > connectors`. Covers runtime lifecycle (`boot`, `start`, `shutdown`, priority, phase, watched files) and the optional static `build.generate` / `build.emit` contribution used by `warlock build`. Triggers: `BaseConnector`, `connectors`, `ConnectorLifecyclePhase`, `ConnectorBuildContribution`, `build.generate`, `build.emit`; "add a queue worker", "wire a subsystem into bootstrap", "contribute files to warlock build", "control startup ordering", "graceful shutdown hook". Skip: app context accessors — the `use-app-context` topic; general config shape — the `configure-app` topic; competing pattern: hand-rolled process signal blocks, NestJS lifecycle hooks.
11
11
  - [benchmark-code](@warlock.js/core/skills/benchmark-code/SKILL.md): Wrap a function with `measure(name, fn, options?)` to time it and classify the latency — onComplete/onError/onFinish hooks, `latencyRange` thresholds, `BenchmarkProfiler` for percentiles, `BenchmarkSnapshots` for raw captures. Triggers: `measure`, `BenchmarkProfiler`, `BenchmarkSnapshots`, `BenchmarkChannel`, `ConsoleChannel`, `latencyRange`, `shouldBenchmarkError`; "time this operation", "profile a slow service", "emit p50/p95/p99 metrics", "classify latency against thresholds"; typical import `import { measure, BenchmarkProfiler } from "@warlock.js/core"`. Skip: retry composition — the `retry-operation` topic; benchmark config wiring — the `configure-app` topic; competing libs `prom-client`, `pino`, `perf_hooks`, `console.time`.
12
+ - [build-budgets](@warlock.js/core/skills/build-budgets/SKILL.md): Configure build.budgets in warlock.config.ts, inspect per-page gzip JS/CSS and deferred assets, compare the last build, and enforce limits with warlock build --json. Use for client bundle size regressions and CI transfer-size gates. For field metrics use web measure-web-vitals.
12
13
  - [build-restful](@warlock.js/core/skills/build-restful/SKILL.md): Generate standard CRUD endpoints — via `router.route(path).list().show().create().update().destroy()` chain or the `Restful` base class. Pick the chain by default; reach for `Restful` when you want repository-bound defaults. Triggers: `router.route`, `Restful`, `router.restfulResource`, `RouteResource`, `.crud`, `.nest`, `beforeCreate`, `onCreate`; "build a CRUD API", "register list/show/create/update/destroy", "repository-bound default handlers", "override a single REST action"; typical import `import { router, Restful } from "@warlock.js/core"`. Skip: wider router surface — the `register-route` topic; per-action controllers — the `create-controller` topic; wire mapping — the `define-resource` topic; competing pattern: hand-rolled controllers, `@nestjs/swagger` decorator-driven CRUD.
13
14
  - [build-url](@warlock.js/core/skills/build-url/SKILL.md): HTTP URL helpers — `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, anchored at `app.baseUrl`. Use to render `src` / `href` / API URLs in resources and responses. `setBaseUrl` is wired by the HTTP connector from `config.get("app.baseUrl")`. Triggers: `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, `setBaseUrl`, `BASE_URL`; "render an avatar src URL", "absolute download link", "embed asset URL in email", "URL helpers vs path helpers"; typical import `import { url, publicUrl, uploadsUrl } from "@warlock.js/core"`. Skip: filesystem paths — the `resolve-path` topic; signed CDN URLs — the `store-file` topic; resource output — the `define-resource` topic; competing patterns: hand-rolled `${baseUrl}/...` template strings.
14
15
  - [configure-app](@warlock.js/core/skills/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app's public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — the `cache-basics` topic of the `warlock-js-cache` skill; mail config — the `send-mail` topic; storage config — the `store-file` topic; sitemap refusal on a missing origin — the `generate-sitemap` topic of the `warlock-js-web` skill; competing libs `dotenv` direct, `convict`, `node-config`.
@@ -17,6 +18,7 @@
17
18
  - [define-resource](@warlock.js/core/skills/define-resource/SKILL.md): Map model fields to wire-shape via `defineResource()` or `Resource` subclasses. Output-only — never put business logic, hydration, or reconciliation in a resource. Also covers the typed output: `ResourceOutput<typeof R>`, `defineResource<Out>()`, `Serialized<T, W>` and `ModelResourceRegistry`. Triggers: `defineResource`, `Resource`, `RegisterResource`, `toJSON`, `ResourceOutput`, `Serialized`, `ModelResourceRegistry`, `"self"`, `"localized"`, `"uploadsUrl"`; "shape an API response", "type a resource's output", "what does the client receive", "nest related resources", "rename a field on output", "self-referential tree resource"; typical import `import { defineResource } from "@warlock.js/core"`. Skip: localized columns — the `use-localization` topic; URL casting — the `build-url` topic; controller side — the `create-controller` topic; competing libs `@nestjs/swagger` `@ApiProperty`, `class-transformer`, hand-rolled DTO mappers.
18
19
  - [encrypt-data](@warlock.js/core/skills/encrypt-data/SKILL.md): Reversible AES-256-GCM `encrypt` / `decrypt` for secrets you need to read back; one-way HMAC-SHA256 `hmacHash` for deterministic fingerprints (lookup/dedup of encrypted columns). Keys come from `src/config/encryption.ts`. Triggers: `encrypt`, `decrypt`, `hmacHash`, `EncryptionConfigurations`, `APP_ENCRYPTION_KEY`, `APP_HMAC_KEY`; "store an API key reversibly", "fingerprint an encrypted column for lookup", "AES-256-GCM secret", "HMAC-SHA256 dedup key"; typical import `import { encrypt, decrypt, hmacHash } from "@warlock.js/core"`. Skip: password hashing — the `hash-password` topic; config wiring — the `configure-app` topic; competing libs Node `crypto` direct, `crypto-js`, `libsodium-wrappers`.
19
20
  - [generate-openapi](@warlock.js/core/skills/generate-openapi/SKILL.md): Generate an OpenAPI 3.1 document from the registered routes with `warlock generate.openapi` (default `storage/openapi/openapi.json`), write a Postman collection from it with `warlock generate.postman` (default `storage/postman/collection.json`), and read it live in the dev-only API docs page at `/__warlock/docs`. Documents paths, path/query parameters, request bodies from Seal validation, responses from `handler.responseSchema`, 401/422 and bearer/cookie security from `authMiddleware()`. Triggers: `generate.openapi`, `openApiGeneratorCommand`, `buildOpenApiDocument`, `getDevelopmentOpenApiDocument`, `generate.postman`, `openApiToPostmanCollection`, `getDevelopmentPostmanCollection`, `responseSchema`, `--include-pages`, `/__warlock/docs`; "OpenAPI", "Swagger", "API docs", "import my API into Postman or Insomnia", "document my routes". Skip: declaring response types on a handler — the `create-controller` topic; the devtools dashboard — the `warlock-js-devtools` skill; listing routes — the `warlock-routes` topic.
21
+ - [generate-web-ui](@warlock.js/core/skills/generate-web-ui/SKILL.md): Generate web pages, schema-backed forms and CRUD UI with warlock generate page, form and crud-ui. Includes setup companions, design-system controls, useSubmitForm, optional guarded endpoints and module CRUD prerequisites.
20
22
  - [hash-password](@warlock.js/core/skills/hash-password/SKILL.md): One-way bcrypt password hashing — `hashPassword` / `verifyPassword`, plus the declarative `useHashedPassword()` schema transformer that auto-hashes a model's password field on save. Salt rounds come from `src/config/encryption.ts`. Triggers: `hashPassword`, `verifyPassword`, `useHashedPassword`, `password.salt`, `bcryptjs`; "hash a user password", "verify login credentials", "auto-hash on save", "rotate a password"; typical import `import { hashPassword, verifyPassword } from "@warlock.js/core"`. Skip: reversible secrets — the `encrypt-data` topic; the other transformers — the `use-model-transformers` topic; config wiring — the `configure-app` topic; competing libs `bcrypt` native, `argon2`, `scrypt`.
21
23
  - [health-checks](@warlock.js/core/skills/health-checks/SKILL.md): Built-in liveness (`/health`) and readiness (`/ready`) endpoints plus graceful HTTP request draining for zero-downtime deploys — the `health` registry (`health.addCheck`/`removeCheck`), the `http.health.*` and `http.gracefulShutdown.*` config, and how readiness ties into `Application.isShuttingDown`. Triggers: `health`, `health.addCheck`, `health.removeCheck`, `HealthCheck`, `/health`, `/ready`, `http.health`, `http.gracefulShutdown`, `forceCloseConnections`, "liveness probe", "readiness probe", "graceful shutdown", "drain in-flight requests", "zero-downtime deploy", "kubernetes health check", "503 until ready"; typical import `import { health } from "@warlock.js/core"`. Skip: the `Application.onShutdown` / `onceBooted` lifecycle hooks — the `use-app-context` topic; maintenance-mode 503s — the `use-middleware` topic; connector lifecycle — the `add-connector` topic; competing libs `@fastify/under-pressure`, `terminus`, hand-rolled `/health` controllers.
22
24
  - [lower-stage3-decorators](@warlock.js/core/skills/lower-stage3-decorators/SKILL.md): Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won't load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core/vite"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — the `test-service` topic / the `test-http` topic; runtime migrations — the `write-migration` topic of the `warlock-js-cascade` skill.
package/package.json CHANGED
@@ -25,12 +25,12 @@
25
25
  "@mongez/slug": "^1.0.7",
26
26
  "@mongez/supportive-is": "^3.0.0",
27
27
  "@mongez/time-wizard": "^1.0.6",
28
- "@warlock.js/cache": "5.34.3",
29
- "@warlock.js/cascade": "5.34.3",
30
- "@warlock.js/context": "5.34.3",
31
- "@warlock.js/fs": "5.34.3",
32
- "@warlock.js/logger": "5.34.3",
33
- "@warlock.js/seal": "5.34.3",
28
+ "@warlock.js/cache": "5.35.0",
29
+ "@warlock.js/cascade": "5.35.0",
30
+ "@warlock.js/context": "5.35.0",
31
+ "@warlock.js/fs": "5.35.0",
32
+ "@warlock.js/logger": "5.35.0",
33
+ "@warlock.js/seal": "5.35.0",
34
34
  "bcryptjs": "^3.0.3",
35
35
  "chokidar": "^5.0.0",
36
36
  "dayjs": "^1.11.19",
@@ -46,7 +46,8 @@
46
46
  "mime": "^4.1.0",
47
47
  "oxc-parser": "0.154.0",
48
48
  "pluralize-esm": "^9.0.5",
49
- "typescript": "7.0.2"
49
+ "typescript": "7.0.2",
50
+ "axe-core": "4.14.0"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "@napi-rs/keyring": "^2.1.1",
@@ -55,12 +56,12 @@
55
56
  "@aws-sdk/lib-storage": "^3.955.0",
56
57
  "@aws-sdk/s3-request-presigner": "^3.955.0",
57
58
  "@react-email/render": "^2.0.5",
58
- "@warlock.js/access": "5.34.3",
59
- "@warlock.js/ai": "5.34.3",
60
- "@warlock.js/ai-tools": "5.34.3",
61
- "@warlock.js/devtools": "5.34.3",
62
- "@warlock.js/herald": "5.34.3",
63
- "@warlock.js/notifications": "5.34.3",
59
+ "@warlock.js/access": "5.35.0",
60
+ "@warlock.js/ai": "5.35.0",
61
+ "@warlock.js/ai-tools": "5.35.0",
62
+ "@warlock.js/devtools": "5.35.0",
63
+ "@warlock.js/herald": "5.35.0",
64
+ "@warlock.js/notifications": "5.35.0",
64
65
  "nodemailer": "^8.0.5",
65
66
  "react": "^19.2.3",
66
67
  "react-dom": "^19.2.3",
@@ -145,7 +146,7 @@
145
146
  "engines": {
146
147
  "node": ">=22.15"
147
148
  },
148
- "version": "5.34.3",
149
+ "version": "5.35.0",
149
150
  "type": "module",
150
151
  "main": "./esm/index.mjs",
151
152
  "module": "./esm/index.mjs",
@@ -47,7 +47,8 @@
47
47
  "pages",
48
48
  "jobs",
49
49
  "schedules",
50
- "packages"
50
+ "packages",
51
+ "files"
51
52
  ]
52
53
  }
53
54
  },
@@ -549,6 +550,51 @@
549
550
  }
550
551
  }
551
552
  }
553
+ },
554
+ "files": {
555
+ "type": "array",
556
+ "items": {
557
+ "type": "object",
558
+ "required": ["file", "module", "role", "exports", "imports", "importedBy"],
559
+ "properties": {
560
+ "file": {
561
+ "type": "string"
562
+ },
563
+ "module": {
564
+ "type": "string"
565
+ },
566
+ "role": {
567
+ "type": "string"
568
+ },
569
+ "exports": {
570
+ "type": "array",
571
+ "items": {
572
+ "type": "string"
573
+ }
574
+ },
575
+ "importedBy": {
576
+ "type": "array",
577
+ "items": {
578
+ "type": "string"
579
+ }
580
+ },
581
+ "imports": {
582
+ "type": "array",
583
+ "items": {
584
+ "type": "object",
585
+ "required": ["specifier", "file"],
586
+ "properties": {
587
+ "specifier": {
588
+ "type": "string"
589
+ },
590
+ "file": {
591
+ "type": ["string", "null"]
592
+ }
593
+ }
594
+ }
595
+ }
596
+ }
597
+ }
552
598
  }
553
599
  }
554
600
  }
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: build-budgets
3
+ description: "Configure build.budgets in warlock.config.ts, inspect per-page gzip JS/CSS and deferred assets, compare the last build, and enforce limits with warlock build --json. Use for client bundle size regressions and CI transfer-size gates. For field metrics use web measure-web-vitals."
4
+ ---
5
+
6
+ # Client build budgets
7
+
8
+ ```ts title="warlock.config.ts"
9
+ import { defineConfig } from "@warlock.js/core";
10
+
11
+ export default defineConfig({
12
+ build: {
13
+ budgets: {
14
+ page: { js: "150kb", css: "50kb" },
15
+ pages: { "/admin/**": { js: "220kb" } },
16
+ mode: "error",
17
+ },
18
+ },
19
+ });
20
+ ```
21
+
22
+ Run `npx warlock build` for the sorted table, or `npx warlock build --json`
23
+ for `{ success, budgets, error? }`. Progress goes to stderr in JSON mode.
24
+ `budgets` contains `version: 1`, `compression: "gzip"`, `pages` and
25
+ `violations`. API-only builds report an empty page list.
26
+
27
+ Page limits cover the hydration entry, page, layouts, root, setup registrations
28
+ and error boundary, plus their transitive static imports. Files are counted once
29
+ per page and gzipped individually. Dynamic imports beyond those required roots
30
+ are reported separately and do not consume the budget. Shared files count in
31
+ each page's first visit. This measures emitted JS/CSS, not HTML, fonts or images.
32
+
33
+ `pages` keys match route paths with `*` (within a segment), `**` (across
34
+ segments), or `?` (one non-slash character). Defaults apply first; matching rules
35
+ merge in insertion order and the last supplied field wins. Unsupported glob
36
+ syntax fails with an actionable error. Units are case-insensitive `b`, `kb` and
37
+ `mb`, with 1024 bytes per kb; equality passes. Omitted fields are unlimited.
38
+
39
+ Existing apps without budgets receive a report only. Configured budgets default
40
+ to `mode: "error"`; `"warn"` logs violations and completes. New project templates
41
+ configure 150kb JS / 50kb CSS. A failure names the page, limit, actual size and
42
+ the ten largest rendered modules. Those module sizes are Rollup attribution
43
+ estimates in uncompressed rendered bytes, not gzip shares; compression is not
44
+ additive across modules inside a file.
45
+
46
+ The successful build stores `build-budgets.json` in `build.outdir` (normally
47
+ `dist`). Preserve that directory before rebuilding to obtain deltas. A failed
48
+ build rolls back to the previous artifact and baseline. JSON output still
49
+ contains its attempted report. Changing output directories or deleting dist
50
+ starts a fresh baseline; new routes display no delta. Vite's private
51
+ `client/.vite/budget-modules.json` contains the full module attribution.
@@ -266,3 +266,59 @@ router.restfulResource("/orders", new OrdersRestful());
266
266
  - The `create-controller` topic — what the chain's handlers look like.
267
267
  - The `define-resource` topic — wire shape for the records the chain returns.
268
268
  - The `warlock-conventions` topic — controller/service/repository layering rules.
269
+
270
+ ## Bounded list endpoints
271
+
272
+ Use `listEndpoint(request, listCallback, options)` from `@warlock.js/core` to
273
+ produce `{ <pluralKey>: data, pagination }` from a page-based repository list.
274
+ The callback receives repository options, with sorting translated into the
275
+ repository's `[column, direction]` tuple. Wrap repository methods in an arrow
276
+ to preserve their receiver. Both `list` and `listCached` work.
277
+
278
+ ```ts
279
+ import { listEndpoint, type Request, type RequestHandler } from "@warlock.js/core";
280
+ import { v, type Infer } from "@warlock.js/seal";
281
+ import { productsRepository } from "../repositories/products.repository";
282
+
283
+ const querySchema = v.object({
284
+ page: v.int().min(1).default(1),
285
+ limit: v.int().min(1).default(20),
286
+ orderBy: v.literal("createdAt", "name").optional(),
287
+ orderDir: v.literal("asc", "desc").optional(),
288
+ status: v.literal("active", "archived").optional(),
289
+ });
290
+ type Query = Infer<typeof querySchema>;
291
+
292
+ export const listProducts: RequestHandler<Request<Query>> = async ({ request, response }) =>
293
+ response.success(
294
+ await listEndpoint(request, (options) => productsRepository.listCached(options), {
295
+ pluralKey: "products",
296
+ maxLimit: 100,
297
+ orderBy: ["createdAt", "name"],
298
+ filters: ["status"],
299
+ }),
300
+ );
301
+
302
+ listProducts.validation = { schema: querySchema, validating: ["query"] };
303
+ // Attach responseSchema with your ProductResource and pagination fields too.
304
+ ```
305
+
306
+ `request.validated()` must already contain schema-validated query values. Seal
307
+ handles coercion and filter validation; the helper does not read raw query or
308
+ body values. Configure the repository's `filterBy` for the chosen filter fields.
309
+ Only explicitly listed `filters` are forwarded; pagination and sort controls
310
+ cannot be declared as filters in the typed API. Filters remain flat repository
311
+ options, rather than a nested `filters` object.
312
+
313
+ Defaults are page 1, limit 20, maxLimit 100. The helper clamps a positive integer
314
+ limit to maxLimit (use `.max(100)` in the query schema instead if you prefer a
315
+ 422 for excessive requests). Invalid page/limit or unlisted sort columns throw
316
+ `AppError("invalidListQuery", 422, ...)`. Sort directions are restricted to
317
+ `asc`/`desc`, defaulting to `asc` when a column is provided. With no column,
318
+ repository default ordering applies. Invalid helper configuration throws a
319
+ `TypeError`. The plural key must be nonempty and cannot be `pagination`.
320
+
321
+ The helper forces page pagination and preserves the repository's pagination
322
+ metadata without recalculating it. Cursor and unpaginated endpoints need their
323
+ own controller. Add authorization constraints inside the callback using trusted
324
+ server values; never accept them as client filters.
@@ -448,3 +448,10 @@ export default {
448
448
  The synchronous formatter receives `HttpErrorResponseContext`: `{ status: number, body: unknown, requestId: string }`. Narrow `body` before reading fields; it is the parsed, serialized JSON body. Return a JSON-serializable object or `undefined` to keep the original. The formatter changes only the body, preserving the HTTP status and headers except for recalculated content length, a `private, no-store` cache policy, and the correlation header. If it throws or cannot serialize its result, core logs the formatter error and retains the original body.
449
449
 
450
450
  The hook covers status 400–599 with `application/json` or an `application/*+json` content type, including Fastify parsing errors, unmatched-route 404s, validation 422s, rate-limit 429s, and 500s. Warlock responses use the ID already stamped from `request.id`. Before Warlock creates a Request, the formatter receives Fastify's `request.id` and echoes that same value under `http.requestId.header` (default `X-Request-Id`). Setting `http.requestId.enabled: false` suppresses that header; without a stamped Warlock ID, the formatter falls back to Fastify's request ID even for routed responses. Keep request-ID headers enabled when the body must correlate with Warlock request logs. HTML pages, files, raw text, streams, and errors after headers have been sent are not reformatted. With no formatter configured, existing response shapes are unchanged.
451
+
452
+ ## Co-located configuration tests
453
+
454
+ `src/config/mail.spec.ts` and `src/config/mail.test.tsx` are never imported as
455
+ configuration. Discovery and reload also exclude `test`, `tests`, `__tests__`,
456
+ `__mocks__`, `__fixtures__`, and `fixtures` directories at any depth. A real
457
+ `src/config/tests.ts` remains ordinary configuration.
@@ -195,3 +195,12 @@ Generated web starter files match the scaffold’s Oxfmt configuration and its
195
195
  built-in import sorting.
196
196
  After editing generated code, run `npm run format` and `npm run lint:check`.
197
197
  Formatting retains unused imports; the linter reports them.
198
+
199
+ ## Notification translation defaults
200
+
201
+ After configuring `app.localeCodes`, run `npx warlock add notifications`.
202
+ New notification dictionaries include `notFound` as a string in every configured
203
+ locale. English ("Notification not found") is the explicit fallback for each
204
+ locale without a bundled translation, including locale sets without English.
205
+ Replace fallback text with your translations before publishing. Existing module
206
+ files and existing translation keys are preserved.
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: generate-web-ui
3
+ description: "Generate web pages, schema-backed forms and CRUD UI with warlock generate page, form and crud-ui. Includes setup companions, design-system controls, useSubmitForm, optional guarded endpoints and module CRUD prerequisites."
4
+ ---
5
+
6
+ # Generate web UI
7
+
8
+ Run these commands from an app with `@warlock.js/web` installed and the app's
9
+ design system in `src/web/design-system/components/`. Controls are default
10
+ exports, imported individually through `web/design-system/components/<name>`;
11
+ they implement Mongez `useFormControl` and accept `name`, `label`, `hint` and
12
+ `error`. Forms import `Form` directly from `@mongez/react-form`.
13
+
14
+ ```sh
15
+ npx warlock generate page account/settings
16
+ npx warlock generate page dashboard --interactive
17
+ npx warlock generate form contact
18
+ npx warlock generate form feedback --module posts --endpoint
19
+ npx warlock generate crud-ui posts --dry-run --json
20
+ npx warlock generate crud-ui posts
21
+ ```
22
+
23
+ `page account/settings` creates `src/web/account/settings.page.tsx` and
24
+ `settings.setup.ts`, with `/account/settings`, a named route, a loader and
25
+ `config.metadata`. `--interactive` also creates a counter feature hook under
26
+ `hooks/`; replace that example with the feature's behavior. Executable page
27
+ exports belong in `.setup.ts`, UI in `.page.tsx`, state and effects in hooks.
28
+
29
+ `form contact` creates `src/web/forms/contact-form.tsx`, a props file, a
30
+ `use-contact-form` hook using `useSubmitForm`, and
31
+ `src/shared/schema/contact.schema.ts`. The starter schema has one `name`
32
+ string field; edit that shared schema and the controls together. Its default
33
+ submit target is `/contact`; supply a real endpoint or set the form's `path`.
34
+ With `--module posts`, output lives under `src/web/posts/`, the shared schema
35
+ under `src/shared/schema/posts/`, and the target is `/posts/contact`.
36
+ The module must exist. `--endpoint` requires `--module`, adds a controller
37
+ with `validation` and `responseSchema`, and appends a guarded POST route.
38
+ This initial endpoint validates and echoes the input; replace that body with a
39
+ service call to persist it. It does not create database persistence for you.
40
+
41
+ For CRUD UI, first generate a full module with
42
+ `npx warlock generate module posts`. Define its create/update schemas and
43
+ list query schema, then run `npx warlock generate crud-ui posts`.
44
+ The generator loads the real Seal schemas using the app loader, so their
45
+ imports must resolve. These schema modules must be safe to import on both
46
+ client and server. Plain string fields map to TextInput, password names to
47
+ PasswordInput, email/date/time/date-time formats to the corresponding controls,
48
+ and booleans to CheckboxInput. Unions, nullable fields, enums, numeric fields,
49
+ nested objects and arrays require custom control decisions and are rejected
50
+ before writing output. An empty module schema produces an empty form.
51
+
52
+ CRUD output contains list/create/edit pages and setup companions, separate
53
+ create/update forms, their hooks and props, list/edit/delete hooks, and a
54
+ delete action component. Lists use the existing GET endpoint and its
55
+ `data`/`pagination` contract (`page` and `pages`). Literal fields in
56
+ `listPostsQuerySchema` become filter inputs; `page` is controlled by navigation
57
+ and `limit` remains the endpoint default. Edit reads GET `/posts/:id`, create
58
+ submits POST `/posts`, update uses PUT `/posts/:id`, and delete uses DELETE
59
+ `/posts/:id`. Successful deletion refreshes the list. Mutation forms show
60
+ validation errors, pending state and success feedback.
61
+
62
+ CRUD generation accepts the conventional full module route file, allowing
63
+ formatting and comments. It fails closed for custom routes/guards instead of
64
+ inferring authorization. Pages require `requireUser({ userType: "user" })`,
65
+ matching the starter's `guarded` helper. Configure `web.session` with the auth
66
+ page-session resolver before visiting these pages. Endpoint guards still
67
+ run on every API request; customized authorization requires a hand-written UI.
68
+
69
+ All three commands support `--dry-run --json` through the normal generation
70
+ report. Existing generated files are skipped unless `--force` is explicit.
71
+ Route registration is idempotent and retains existing module content.
package/skills/index.md CHANGED
@@ -18,11 +18,11 @@ When the framework is wrong or missing a capability, follow the `report-framewor
18
18
 
19
19
  ## Topics by area
20
20
 
21
- - **Foundations:** warlock-conventions, create-module, make-endpoint, configure-app, use-app-context, resolve-path, build-url, update-packages, lower-stage3-decorators, upgrade-from-5-29
21
+ - **Foundations:** warlock-conventions, create-module, make-endpoint, configure-app, build-budgets, use-app-context, resolve-path, build-url, update-packages, lower-stage3-decorators, upgrade-from-5-29
22
22
  - **HTTP & routing:** register-route, warlock-routes, create-controller, use-middleware, write-middleware, validate-input, use-request-locals, build-restful, wire-socket
23
23
  - **Responses & resources:** send-response, define-resource, use-model-transformers, use-localization
24
24
  - **Domain layers:** write-use-case, use-repository, write-seeder
25
- - **CLI & running the app:** run-app, write-cli-command, add-connector, health-checks, warlock-doctor, warlock-inspect, warlock-check, generate-openapi
25
+ - **CLI & running the app:** generate-web-ui, run-app, write-cli-command, add-connector, health-checks, warlock-doctor, warlock-inspect, warlock-check, generate-openapi
26
26
  - **Agent tooling:** warlock-mcp, warlock-trace, warlock-agents-md, warlock-board
27
27
  - **Storage, files & mail:** store-file, upload-file, process-image, send-mail
28
28
  - **Security & utilities:** hash-password, encrypt-data, store-secrets, retry-operation, benchmark-code, request-memo, request-tracing
@@ -182,7 +182,7 @@ Omit switches to run TypeScript and installed/configured Oxlint. Set `oxlint: fa
182
182
 
183
183
  ## `warlock build` — production bundle
184
184
 
185
- esbuild bundle of the app down to a single JS file in `dist/`. No flags — every setting comes from `warlock.config.ts > build`.
185
+ esbuild bundle of the app down to a single JS file in `dist/`. `--json` prints the client budget report; build settings come from `warlock.config.ts > build`. See the `build-budgets` topic for per-page gzip limits and deltas.
186
186
 
187
187
  ### `build.*` config knobs
188
188
 
@@ -400,3 +400,28 @@ await Mail.to(user.email)
400
400
 
401
401
  - The `configure-app` topic — `src/config/mail.ts` shape and `env()` patterns.
402
402
  - The `warlock-conventions` topic — where mail-sending services live.
403
+
404
+ ## Preview development mail in devtools
405
+
406
+ Run `npx warlock add devtools`, start development, and open `/__warlock` (or your
407
+ configured devtools path). In the mail panel, select a captured message to read
408
+ its recipients, subject, text, and sandboxed HTML preview. Send while the panel's
409
+ collector is attached; captures are held in memory and disappear on restart.
410
+
411
+ Use development mail mode so previews do not deliver real email:
412
+
413
+ ```ts
414
+ import { setMailMode, sendMail } from "@warlock.js/core";
415
+
416
+ setMailMode("development");
417
+ await sendMail({
418
+ to: "reader@example.com",
419
+ subject: "Preview",
420
+ html: "<p>Your code is 123456</p>",
421
+ });
422
+ ```
423
+
424
+ Development sends emit `mailEvents.onCaptured`, which devtools consumes; console
425
+ logs show metadata rather than sensitive bodies. Devtools is mounted only for
426
+ the development runtime and is not mounted in production. Keep production mail
427
+ mode configured for actual delivery. Test-mode assertions use the test mailbox.
@@ -508,3 +508,53 @@ export default {
508
508
  The synchronous formatter receives `HttpErrorResponseContext`: `{ status: number, body: unknown, requestId: string }`. Narrow `body` before reading fields; it is the parsed, serialized JSON body. Return a JSON-serializable object or `undefined` to keep the original. The formatter changes only the body, preserving the HTTP status and headers except for recalculated content length, a `private, no-store` cache policy, and the correlation header. If it throws or cannot serialize its result, core logs the formatter error and retains the original body.
509
509
 
510
510
  The hook covers status 400–599 with `application/json` or an `application/*+json` content type, including Fastify parsing errors, unmatched-route 404s, validation 422s, rate-limit 429s, and 500s. Warlock responses use the ID already stamped from `request.id`. Before Warlock creates a Request, the formatter receives Fastify's `request.id` and echoes that same value under `http.requestId.header` (default `X-Request-Id`). Setting `http.requestId.enabled: false` suppresses that header; without a stamped Warlock ID, the formatter falls back to Fastify's request ID even for routed responses. Keep request-ID headers enabled when the body must correlate with Warlock request logs. HTML pages, files, raw text, streams, and errors after headers have been sent are not reformatted. With no formatter configured, existing response shapes are unchanged.
511
+
512
+ ## Typed domain failures
513
+
514
+ `AppError<Details, Code>` is available from `@warlock.js/core` and the lightweight
515
+ `@warlock.js/core/errors` entry. Its constructor is
516
+ `new AppError(code, status, publicMessage, details?)`. Status must be an integer
517
+ from 400 to 599. The message and details are explicitly public, including for
518
+ 5xx errors: never put diagnostics, credentials or private records there.
519
+
520
+ ```ts
521
+ import { AppError } from "@warlock.js/core/errors";
522
+
523
+ throw new AppError("quotaExceeded", 429, "Try again later.", { retryAfter: 30 });
524
+ ```
525
+
526
+ The default response is
527
+ `{ error: { code: "quotaExceeded", message: "Try again later.", details: { retryAfter: 30 } } }`.
528
+ Absent details are omitted; `false`, `0` and `null` details are preserved. Errors
529
+ retain the private, no-store cache policy, and server failures are logged.
530
+
531
+ `AppError` and `HttpError` are separate `Error` subclasses. Existing `HttpError`
532
+ constructors, inheritance, `payload` and legacy `{ error: message, payload? }`
533
+ bodies remain unchanged. Both bypass `http.errorMapper`; ordinary errors retain
534
+ the existing opaque 500 fallback. Choose `AppError` for new domain failures
535
+ that need stable codes, and keep existing HTTP error subclasses as they are.
536
+
537
+ `http.errorResponse` receives an optional typed `appError` alongside `body`,
538
+ `status` and `requestId`. No per-error mapper is needed:
539
+
540
+ ```ts
541
+ import type { HttpErrorResponseFormatter } from "@warlock.js/core";
542
+
543
+ const errorResponse: HttpErrorResponseFormatter = ({ appError, requestId }) => {
544
+ if (!appError) return undefined; // keep other JSON errors unchanged
545
+ return {
546
+ error: {
547
+ code: appError.code,
548
+ message: appError.publicMessage,
549
+ ...(appError.details === undefined ? {} : { details: appError.details }),
550
+ },
551
+ requestId,
552
+ };
553
+ };
554
+ // Set this function as http.errorResponse in your HTTP configuration.
555
+ ```
556
+
557
+ The formatter sees the original instance only for thrown `AppError`s handled
558
+ by the request lifecycle. Early Fastify rejections and returned helper responses
559
+ have no `appError`; use `body` and `status` for those cases. Do not spread an
560
+ error instance into the response.
@@ -118,3 +118,56 @@ Cleanup verifies the unique supervisor command line before stopping only its
118
118
  recorded process group (Windows: verified PID tree). It retains disposable files
119
119
  if ownership verification fails and reports the failure in evidence. Do not add
120
120
  process-name or port-based kills to a spec.
121
+
122
+ ## Accessibility checks
123
+
124
+ Call `await page.assertAccessible()` after the content is visible and again after
125
+ opening menus, dialogs or validation feedback. The harness reads its pinned local
126
+ axe-core distribution and injects it with CDP `Runtime.evaluate`; it never fetches
127
+ a CDN script or adds axe to the app bundle. A missing dependency, injection error,
128
+ unmatched selector or scan exception fails the step.
129
+
130
+ ```ts
131
+ await page.assertAccessible();
132
+ await page.assertAccessible({
133
+ selector: "main",
134
+ tags: ["wcag2a", "wcag2aa", "wcag21aa"],
135
+ allow: [{ id: "color-contrast", selector: ".legacy-caption" }],
136
+ timeoutMs: 30_000,
137
+ });
138
+ ```
139
+
140
+ Defaults run all enabled axe rules on the document. `tags` uses axe's `runOnly`
141
+ tag filter; `selector` limits the scanned region. Allowances match exact rule IDs
142
+ and, when supplied, an exact single-element target selector. Omitting `selector`
143
+ from an allowance permits that rule everywhere; keep exceptions narrow. Nested
144
+ frame/shadow targets cannot match a simple selector allowance. Allowed nodes stay
145
+ in evidence with `allowed: true`; other nodes of the same rule still fail.
146
+
147
+ Evidence's `accessibility` array contains URL, viewport, timestamp, violations
148
+ with rule IDs and target selectors, and incomplete checks for manual review.
149
+ Incomplete checks do not fail automatically. A passing automated scan does not
150
+ replace keyboard, screen-reader, focus-order or contrast review. See the
151
+ [axe API](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md) for scan
152
+ semantics. Run at both 390 and 1280 pixels with `assertViewport()` as well.
153
+
154
+ ## Watched development checks
155
+
156
+ Use an owned app copy with `app.env: { NODE_ENV: "development" }` and
157
+ `app.start: [process.execPath, "node_modules/@warlock.js/core/bin/warlock.js", "dev"]`.
158
+ Do not attach to an existing server. The fixture's `directory` is available in
159
+ `run`; use `node:fs/promises` to edit only files inside that copy.
160
+
161
+ Wait for a visible revision marker after each edit; a successful file write alone
162
+ does not prove watcher invalidation or browser hydration. Exercise several edits,
163
+ including setup-file creation, changes and removal. After each revision, request
164
+ the page again, exercise a hydrated control, check imported `shared` in loaders
165
+ and middleware, and verify concurrent requests keep distinct values. Do not
166
+ allowlist request-store or hydration errors to make these checks pass.
167
+
168
+ For forms beneath layouts, compare server-rendered IDs with the hydrated hook
169
+ values (for example expose the hook value in a click handler), check repeated
170
+ instances, label `for` and ARIA references, then navigate away and back. Comparing
171
+ DOM IDs alone can miss a mismatch: React may retain the server attribute while
172
+ client handlers capture another ID. Capture evidence and screenshots at every
173
+ revision. Do not replace React's `useId()` globally or hide hydration warnings.
@@ -458,3 +458,94 @@ This is fine for normal test flow. It bites when you're inside a transaction the
458
458
  - The `create-controller` topic — the controller shape your HTTP tests are exercising.
459
459
  - The `warlock-conventions` topic — module layout, where `tests/*.test.ts` files live.
460
460
  - The `write-cli-command` topic — `warlock add test` scaffolds both global + worker setup files.
461
+
462
+ ## Worker dictionaries and owned runtime resources
463
+
464
+ The generated `src/test-setup.ts` calls:
465
+
466
+ ```ts
467
+ await setupTest({ importModule: (file) => import(file) });
468
+ ```
469
+
470
+ `setupTest()` reads `src/app/*/utils/locales.json` after configuration in each
471
+ Vitest worker. Server translation helpers therefore see all configured locales
472
+ without manual registration. Invalid dictionaries fail setup. `teardownTest()`
473
+ unregisters those dictionaries; legacy executable locale files still require
474
+ explicit imports in worker setup.
475
+
476
+ Global HTTP fixtures should use `await startHttpTestServer({ port: 0, web: false })`
477
+ for API-only suites and `await stopHttpTestServer()` in teardown. Stop releases
478
+ connectors and the file runtime's loader channel, queued work, watchers, health
479
+ workers, and registered module cleanup hooks, including after partial startup.
480
+ A Node loader registration is process-scoped; its message channel is closed at
481
+ teardown. Module-owned timers/listeners must register their cleanup hooks.
482
+
483
+ A module that owns a long-running handle can export an awaited cleanup:
484
+
485
+ ```ts
486
+ // src/app/messages/main.ts
487
+ import { createServer } from "node:net";
488
+ const listener = createServer();
489
+ listener.listen(0, "127.0.0.1");
490
+ export async function cleanup(): Promise<void> {
491
+ await new Promise<void>((resolve, reject) => {
492
+ listener.close((error) => (error ? reject(error) : resolve()));
493
+ });
494
+ }
495
+ ```
496
+
497
+ HTTP test teardown awaits these cleanup functions and reports failures after
498
+ attempting every module's cleanup. Avoid leaving module-owned handles running
499
+ outside this lifecycle.
500
+
501
+ ## Response contracts from declared schemas
502
+
503
+ `assertResponseContracts` from `@warlock.js/core/tests` sends HTTP requests and
504
+ asserts the serialized JSON body against the handler's `responseSchema` for the
505
+ actual status. It reuses Core's OpenAPI schema translation, including resource
506
+ classes, nested objects, arrays, nullable casts and resource self references.
507
+ It checks required fields and primitive types without coercion; extra fields
508
+ are allowed. URL/date format annotations are not checked beyond their declared
509
+ JSON shape. Builders, computed resolvers and resources without a static shape
510
+ fail with a diagnostic rather than silently passing.
511
+
512
+ ```ts
513
+ import { it } from "vitest";
514
+ import { router } from "@warlock.js/core";
515
+ import { assertResponseContracts } from "@warlock.js/core/tests";
516
+
517
+ it("matches product response declarations", async () => {
518
+ await assertResponseContracts({
519
+ routes: router.list().filter((route) => route.path === "/products"),
520
+ cases: () => [{ path: "/products?page=1&limit=10", status: 200 }],
521
+ requireAllStatuses: true,
522
+ });
523
+ });
524
+ ```
525
+
526
+ With the shared test server, retain `startHttpTestServer({ port: 0 })` in global
527
+ setup and `setupTest` in the worker. The routes and their handler schemas must
528
+ also be loaded in the worker running this assertion (import their routes module
529
+ there); global setup runs in a different process. Omit `routes` to discover all
530
+ registered routes with `responseSchema` in that worker. Empty discovery fails.
531
+ Explicit routes lacking a schema also fail.
532
+
533
+ Static GET routes default to one request without headers. Parameterized routes
534
+ and non-GET methods require explicit `cases(route)` fixtures; an empty case
535
+ list fails. Each case can supply a concrete `path`, `init` (headers, serialized
536
+ body, etc.) and expected `status`. Supply fixtures with valid IDs, seeded data,
537
+ authentication and invalid inputs to exercise the desired outcomes. An `all` route also needs a concrete fixture `method`. Redirects
538
+ are not followed. POST/PUT/PATCH/DELETE fixtures execute real side effects;
539
+ use isolated test data and clean it up.
540
+
541
+ Undeclared response statuses fail. Only statuses actually returned are checked
542
+ by default; `requireAllStatuses: true` additionally fails if any declared status
543
+ has no exercised fixture. HEAD, 204 and 304 responses must have an empty body; declare it with
544
+ `{ body: {} }`. Other statuses must carry matching JSON.
545
+
546
+ For an in-process server, pass `client: app` from `createTestHttpServer()` and
547
+ close it in teardown. For signed-in requests, pass `client: await actingAs(user)`
548
+ and dispose that client afterward. These clients use the same assertion and
549
+ run the real HTTP middleware and serialization paths.
550
+
551
+ Cleanup hooks that own asynchronous handles should return a promise for completed closure. For MessagePorts, subscribe to `close` before calling `close()` and await the event before checking reference state.