@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-full.txt CHANGED
@@ -26,11 +26,11 @@ When the framework is wrong or missing a capability, follow the `report-framewor
26
26
 
27
27
  ## Topics by area
28
28
 
29
- - **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
29
+ - **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
30
30
  - **HTTP & routing:** register-route, warlock-routes, create-controller, use-middleware, write-middleware, validate-input, use-request-locals, build-restful, wire-socket
31
31
  - **Responses & resources:** send-response, define-resource, use-model-transformers, use-localization
32
32
  - **Domain layers:** write-use-case, use-repository, write-seeder
33
- - **CLI & running the app:** run-app, write-cli-command, add-connector, health-checks, warlock-doctor, warlock-inspect, warlock-check, generate-openapi
33
+ - **CLI & running the app:** generate-web-ui, run-app, write-cli-command, add-connector, health-checks, warlock-doctor, warlock-inspect, warlock-check, generate-openapi
34
34
  - **Agent tooling:** warlock-mcp, warlock-trace, warlock-agents-md, warlock-board
35
35
  - **Storage, files & mail:** store-file, upload-file, process-image, send-mail
36
36
  - **Security & utilities:** hash-password, encrypt-data, store-secrets, retry-operation, benchmark-code, request-memo, request-tracing
@@ -543,6 +543,61 @@ The `latency` is the _total_ time including retries — useful for the SLO you a
543
543
  - The `configure-app` topic — wiring `src/config/benchmark.ts`.
544
544
 
545
545
 
546
+ ## build-budgets `@warlock.js/core/skills/build-budgets/SKILL.md`
547
+
548
+ ---
549
+ name: build-budgets
550
+ 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."
551
+ ---
552
+
553
+ # Client build budgets
554
+
555
+ ```ts title="warlock.config.ts"
556
+ import { defineConfig } from "@warlock.js/core";
557
+
558
+ export default defineConfig({
559
+ build: {
560
+ budgets: {
561
+ page: { js: "150kb", css: "50kb" },
562
+ pages: { "/admin/**": { js: "220kb" } },
563
+ mode: "error",
564
+ },
565
+ },
566
+ });
567
+ ```
568
+
569
+ Run `npx warlock build` for the sorted table, or `npx warlock build --json`
570
+ for `{ success, budgets, error? }`. Progress goes to stderr in JSON mode.
571
+ `budgets` contains `version: 1`, `compression: "gzip"`, `pages` and
572
+ `violations`. API-only builds report an empty page list.
573
+
574
+ Page limits cover the hydration entry, page, layouts, root, setup registrations
575
+ and error boundary, plus their transitive static imports. Files are counted once
576
+ per page and gzipped individually. Dynamic imports beyond those required roots
577
+ are reported separately and do not consume the budget. Shared files count in
578
+ each page's first visit. This measures emitted JS/CSS, not HTML, fonts or images.
579
+
580
+ `pages` keys match route paths with `*` (within a segment), `**` (across
581
+ segments), or `?` (one non-slash character). Defaults apply first; matching rules
582
+ merge in insertion order and the last supplied field wins. Unsupported glob
583
+ syntax fails with an actionable error. Units are case-insensitive `b`, `kb` and
584
+ `mb`, with 1024 bytes per kb; equality passes. Omitted fields are unlimited.
585
+
586
+ Existing apps without budgets receive a report only. Configured budgets default
587
+ to `mode: "error"`; `"warn"` logs violations and completes. New project templates
588
+ configure 150kb JS / 50kb CSS. A failure names the page, limit, actual size and
589
+ the ten largest rendered modules. Those module sizes are Rollup attribution
590
+ estimates in uncompressed rendered bytes, not gzip shares; compression is not
591
+ additive across modules inside a file.
592
+
593
+ The successful build stores `build-budgets.json` in `build.outdir` (normally
594
+ `dist`). Preserve that directory before rebuilding to obtain deltas. A failed
595
+ build rolls back to the previous artifact and baseline. JSON output still
596
+ contains its attempted report. Changing output directories or deleting dist
597
+ starts a fresh baseline; new routes display no delta. Vite's private
598
+ `client/.vite/budget-modules.json` contains the full module attribution.
599
+
600
+
546
601
  ## build-restful `@warlock.js/core/skills/build-restful/SKILL.md`
547
602
 
548
603
  ---
@@ -814,6 +869,62 @@ router.restfulResource("/orders", new OrdersRestful());
814
869
  - The `define-resource` topic — wire shape for the records the chain returns.
815
870
  - The `warlock-conventions` topic — controller/service/repository layering rules.
816
871
 
872
+ ## Bounded list endpoints
873
+
874
+ Use `listEndpoint(request, listCallback, options)` from `@warlock.js/core` to
875
+ produce `{ <pluralKey>: data, pagination }` from a page-based repository list.
876
+ The callback receives repository options, with sorting translated into the
877
+ repository's `[column, direction]` tuple. Wrap repository methods in an arrow
878
+ to preserve their receiver. Both `list` and `listCached` work.
879
+
880
+ ```ts
881
+ import { listEndpoint, type Request, type RequestHandler } from "@warlock.js/core";
882
+ import { v, type Infer } from "@warlock.js/seal";
883
+ import { productsRepository } from "../repositories/products.repository";
884
+
885
+ const querySchema = v.object({
886
+ page: v.int().min(1).default(1),
887
+ limit: v.int().min(1).default(20),
888
+ orderBy: v.literal("createdAt", "name").optional(),
889
+ orderDir: v.literal("asc", "desc").optional(),
890
+ status: v.literal("active", "archived").optional(),
891
+ });
892
+ type Query = Infer<typeof querySchema>;
893
+
894
+ export const listProducts: RequestHandler<Request<Query>> = async ({ request, response }) =>
895
+ response.success(
896
+ await listEndpoint(request, (options) => productsRepository.listCached(options), {
897
+ pluralKey: "products",
898
+ maxLimit: 100,
899
+ orderBy: ["createdAt", "name"],
900
+ filters: ["status"],
901
+ }),
902
+ );
903
+
904
+ listProducts.validation = { schema: querySchema, validating: ["query"] };
905
+ // Attach responseSchema with your ProductResource and pagination fields too.
906
+ ```
907
+
908
+ `request.validated()` must already contain schema-validated query values. Seal
909
+ handles coercion and filter validation; the helper does not read raw query or
910
+ body values. Configure the repository's `filterBy` for the chosen filter fields.
911
+ Only explicitly listed `filters` are forwarded; pagination and sort controls
912
+ cannot be declared as filters in the typed API. Filters remain flat repository
913
+ options, rather than a nested `filters` object.
914
+
915
+ Defaults are page 1, limit 20, maxLimit 100. The helper clamps a positive integer
916
+ limit to maxLimit (use `.max(100)` in the query schema instead if you prefer a
917
+ 422 for excessive requests). Invalid page/limit or unlisted sort columns throw
918
+ `AppError("invalidListQuery", 422, ...)`. Sort directions are restricted to
919
+ `asc`/`desc`, defaulting to `asc` when a column is provided. With no column,
920
+ repository default ordering applies. Invalid helper configuration throws a
921
+ `TypeError`. The plural key must be nonempty and cannot be `pagination`.
922
+
923
+ The helper forces page pagination and preserves the repository's pagination
924
+ metadata without recalculating it. Cursor and unpaginated endpoints need their
925
+ own controller. Add authorization constraints inside the callback using trusted
926
+ server values; never accept them as client filters.
927
+
817
928
 
818
929
  ## build-url `@warlock.js/core/skills/build-url/SKILL.md`
819
930
 
@@ -1452,6 +1563,13 @@ The synchronous formatter receives `HttpErrorResponseContext`: `{ status: number
1452
1563
 
1453
1564
  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.
1454
1565
 
1566
+ ## Co-located configuration tests
1567
+
1568
+ `src/config/mail.spec.ts` and `src/config/mail.test.tsx` are never imported as
1569
+ configuration. Discovery and reload also exclude `test`, `tests`, `__tests__`,
1570
+ `__mocks__`, `__fixtures__`, and `fixtures` directories at any depth. A real
1571
+ `src/config/tests.ts` remains ordinary configuration.
1572
+
1455
1573
 
1456
1574
  ## create-controller `@warlock.js/core/skills/create-controller/SKILL.md`
1457
1575
 
@@ -1952,6 +2070,15 @@ built-in import sorting.
1952
2070
  After editing generated code, run `npm run format` and `npm run lint:check`.
1953
2071
  Formatting retains unused imports; the linter reports them.
1954
2072
 
2073
+ ## Notification translation defaults
2074
+
2075
+ After configuring `app.localeCodes`, run `npx warlock add notifications`.
2076
+ New notification dictionaries include `notFound` as a string in every configured
2077
+ locale. English ("Notification not found") is the explicit fallback for each
2078
+ locale without a bundled translation, including locale sets without English.
2079
+ Replace fallback text with your translations before publishing. Existing module
2080
+ files and existing translation keys are preserved.
2081
+
1955
2082
 
1956
2083
  ## define-resource `@warlock.js/core/skills/define-resource/SKILL.md`
1957
2084
 
@@ -2752,6 +2879,81 @@ const { document, warnings } = buildOpenApiDocument(router.list(), {
2752
2879
  - The `warlock-routes` topic — list the routes the document is built from.
2753
2880
 
2754
2881
 
2882
+ ## generate-web-ui `@warlock.js/core/skills/generate-web-ui/SKILL.md`
2883
+
2884
+ ---
2885
+ name: generate-web-ui
2886
+ 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."
2887
+ ---
2888
+
2889
+ # Generate web UI
2890
+
2891
+ Run these commands from an app with `@warlock.js/web` installed and the app's
2892
+ design system in `src/web/design-system/components/`. Controls are default
2893
+ exports, imported individually through `web/design-system/components/<name>`;
2894
+ they implement Mongez `useFormControl` and accept `name`, `label`, `hint` and
2895
+ `error`. Forms import `Form` directly from `@mongez/react-form`.
2896
+
2897
+ ```sh
2898
+ npx warlock generate page account/settings
2899
+ npx warlock generate page dashboard --interactive
2900
+ npx warlock generate form contact
2901
+ npx warlock generate form feedback --module posts --endpoint
2902
+ npx warlock generate crud-ui posts --dry-run --json
2903
+ npx warlock generate crud-ui posts
2904
+ ```
2905
+
2906
+ `page account/settings` creates `src/web/account/settings.page.tsx` and
2907
+ `settings.setup.ts`, with `/account/settings`, a named route, a loader and
2908
+ `config.metadata`. `--interactive` also creates a counter feature hook under
2909
+ `hooks/`; replace that example with the feature's behavior. Executable page
2910
+ exports belong in `.setup.ts`, UI in `.page.tsx`, state and effects in hooks.
2911
+
2912
+ `form contact` creates `src/web/forms/contact-form.tsx`, a props file, a
2913
+ `use-contact-form` hook using `useSubmitForm`, and
2914
+ `src/shared/schema/contact.schema.ts`. The starter schema has one `name`
2915
+ string field; edit that shared schema and the controls together. Its default
2916
+ submit target is `/contact`; supply a real endpoint or set the form's `path`.
2917
+ With `--module posts`, output lives under `src/web/posts/`, the shared schema
2918
+ under `src/shared/schema/posts/`, and the target is `/posts/contact`.
2919
+ The module must exist. `--endpoint` requires `--module`, adds a controller
2920
+ with `validation` and `responseSchema`, and appends a guarded POST route.
2921
+ This initial endpoint validates and echoes the input; replace that body with a
2922
+ service call to persist it. It does not create database persistence for you.
2923
+
2924
+ For CRUD UI, first generate a full module with
2925
+ `npx warlock generate module posts`. Define its create/update schemas and
2926
+ list query schema, then run `npx warlock generate crud-ui posts`.
2927
+ The generator loads the real Seal schemas using the app loader, so their
2928
+ imports must resolve. These schema modules must be safe to import on both
2929
+ client and server. Plain string fields map to TextInput, password names to
2930
+ PasswordInput, email/date/time/date-time formats to the corresponding controls,
2931
+ and booleans to CheckboxInput. Unions, nullable fields, enums, numeric fields,
2932
+ nested objects and arrays require custom control decisions and are rejected
2933
+ before writing output. An empty module schema produces an empty form.
2934
+
2935
+ CRUD output contains list/create/edit pages and setup companions, separate
2936
+ create/update forms, their hooks and props, list/edit/delete hooks, and a
2937
+ delete action component. Lists use the existing GET endpoint and its
2938
+ `data`/`pagination` contract (`page` and `pages`). Literal fields in
2939
+ `listPostsQuerySchema` become filter inputs; `page` is controlled by navigation
2940
+ and `limit` remains the endpoint default. Edit reads GET `/posts/:id`, create
2941
+ submits POST `/posts`, update uses PUT `/posts/:id`, and delete uses DELETE
2942
+ `/posts/:id`. Successful deletion refreshes the list. Mutation forms show
2943
+ validation errors, pending state and success feedback.
2944
+
2945
+ CRUD generation accepts the conventional full module route file, allowing
2946
+ formatting and comments. It fails closed for custom routes/guards instead of
2947
+ inferring authorization. Pages require `requireUser({ userType: "user" })`,
2948
+ matching the starter's `guarded` helper. Configure `web.session` with the auth
2949
+ page-session resolver before visiting these pages. Endpoint guards still
2950
+ run on every API request; customized authorization requires a hand-written UI.
2951
+
2952
+ All three commands support `--dry-run --json` through the normal generation
2953
+ report. Existing generated files are skipped unless `--force` is explicit.
2954
+ Route registration is idempotent and retains existing module content.
2955
+
2956
+
2755
2957
  ## hash-password `@warlock.js/core/skills/hash-password/SKILL.md`
2756
2958
 
2757
2959
  ---
@@ -4843,7 +5045,7 @@ Omit switches to run TypeScript and installed/configured Oxlint. Set `oxlint: fa
4843
5045
 
4844
5046
  ## `warlock build` — production bundle
4845
5047
 
4846
- esbuild bundle of the app down to a single JS file in `dist/`. No flags — every setting comes from `warlock.config.ts > build`.
5048
+ 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.
4847
5049
 
4848
5050
  ### `build.*` config knobs
4849
5051
 
@@ -5782,6 +5984,31 @@ await Mail.to(user.email)
5782
5984
  - The `configure-app` topic — `src/config/mail.ts` shape and `env()` patterns.
5783
5985
  - The `warlock-conventions` topic — where mail-sending services live.
5784
5986
 
5987
+ ## Preview development mail in devtools
5988
+
5989
+ Run `npx warlock add devtools`, start development, and open `/__warlock` (or your
5990
+ configured devtools path). In the mail panel, select a captured message to read
5991
+ its recipients, subject, text, and sandboxed HTML preview. Send while the panel's
5992
+ collector is attached; captures are held in memory and disappear on restart.
5993
+
5994
+ Use development mail mode so previews do not deliver real email:
5995
+
5996
+ ```ts
5997
+ import { setMailMode, sendMail } from "@warlock.js/core";
5998
+
5999
+ setMailMode("development");
6000
+ await sendMail({
6001
+ to: "reader@example.com",
6002
+ subject: "Preview",
6003
+ html: "<p>Your code is 123456</p>",
6004
+ });
6005
+ ```
6006
+
6007
+ Development sends emit `mailEvents.onCaptured`, which devtools consumes; console
6008
+ logs show metadata rather than sensitive bodies. Devtools is mounted only for
6009
+ the development runtime and is not mounted in production. Keep production mail
6010
+ mode configured for actual delivery. Test-mode assertions use the test mailbox.
6011
+
5785
6012
 
5786
6013
  ## send-response `@warlock.js/core/skills/send-response/SKILL.md`
5787
6014
 
@@ -6296,6 +6523,56 @@ The synchronous formatter receives `HttpErrorResponseContext`: `{ status: number
6296
6523
 
6297
6524
  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.
6298
6525
 
6526
+ ## Typed domain failures
6527
+
6528
+ `AppError<Details, Code>` is available from `@warlock.js/core` and the lightweight
6529
+ `@warlock.js/core/errors` entry. Its constructor is
6530
+ `new AppError(code, status, publicMessage, details?)`. Status must be an integer
6531
+ from 400 to 599. The message and details are explicitly public, including for
6532
+ 5xx errors: never put diagnostics, credentials or private records there.
6533
+
6534
+ ```ts
6535
+ import { AppError } from "@warlock.js/core/errors";
6536
+
6537
+ throw new AppError("quotaExceeded", 429, "Try again later.", { retryAfter: 30 });
6538
+ ```
6539
+
6540
+ The default response is
6541
+ `{ error: { code: "quotaExceeded", message: "Try again later.", details: { retryAfter: 30 } } }`.
6542
+ Absent details are omitted; `false`, `0` and `null` details are preserved. Errors
6543
+ retain the private, no-store cache policy, and server failures are logged.
6544
+
6545
+ `AppError` and `HttpError` are separate `Error` subclasses. Existing `HttpError`
6546
+ constructors, inheritance, `payload` and legacy `{ error: message, payload? }`
6547
+ bodies remain unchanged. Both bypass `http.errorMapper`; ordinary errors retain
6548
+ the existing opaque 500 fallback. Choose `AppError` for new domain failures
6549
+ that need stable codes, and keep existing HTTP error subclasses as they are.
6550
+
6551
+ `http.errorResponse` receives an optional typed `appError` alongside `body`,
6552
+ `status` and `requestId`. No per-error mapper is needed:
6553
+
6554
+ ```ts
6555
+ import type { HttpErrorResponseFormatter } from "@warlock.js/core";
6556
+
6557
+ const errorResponse: HttpErrorResponseFormatter = ({ appError, requestId }) => {
6558
+ if (!appError) return undefined; // keep other JSON errors unchanged
6559
+ return {
6560
+ error: {
6561
+ code: appError.code,
6562
+ message: appError.publicMessage,
6563
+ ...(appError.details === undefined ? {} : { details: appError.details }),
6564
+ },
6565
+ requestId,
6566
+ };
6567
+ };
6568
+ // Set this function as http.errorResponse in your HTTP configuration.
6569
+ ```
6570
+
6571
+ The formatter sees the original instance only for thrown `AppError`s handled
6572
+ by the request lifecycle. Early Fastify rejections and returned helper responses
6573
+ have no `appError`; use `body` and `status` for those cases. Do not spread an
6574
+ error instance into the response.
6575
+
6299
6576
 
6300
6577
  ## store-file `@warlock.js/core/skills/store-file/SKILL.md`
6301
6578
 
@@ -7032,6 +7309,59 @@ recorded process group (Windows: verified PID tree). It retains disposable files
7032
7309
  if ownership verification fails and reports the failure in evidence. Do not add
7033
7310
  process-name or port-based kills to a spec.
7034
7311
 
7312
+ ## Accessibility checks
7313
+
7314
+ Call `await page.assertAccessible()` after the content is visible and again after
7315
+ opening menus, dialogs or validation feedback. The harness reads its pinned local
7316
+ axe-core distribution and injects it with CDP `Runtime.evaluate`; it never fetches
7317
+ a CDN script or adds axe to the app bundle. A missing dependency, injection error,
7318
+ unmatched selector or scan exception fails the step.
7319
+
7320
+ ```ts
7321
+ await page.assertAccessible();
7322
+ await page.assertAccessible({
7323
+ selector: "main",
7324
+ tags: ["wcag2a", "wcag2aa", "wcag21aa"],
7325
+ allow: [{ id: "color-contrast", selector: ".legacy-caption" }],
7326
+ timeoutMs: 30_000,
7327
+ });
7328
+ ```
7329
+
7330
+ Defaults run all enabled axe rules on the document. `tags` uses axe's `runOnly`
7331
+ tag filter; `selector` limits the scanned region. Allowances match exact rule IDs
7332
+ and, when supplied, an exact single-element target selector. Omitting `selector`
7333
+ from an allowance permits that rule everywhere; keep exceptions narrow. Nested
7334
+ frame/shadow targets cannot match a simple selector allowance. Allowed nodes stay
7335
+ in evidence with `allowed: true`; other nodes of the same rule still fail.
7336
+
7337
+ Evidence's `accessibility` array contains URL, viewport, timestamp, violations
7338
+ with rule IDs and target selectors, and incomplete checks for manual review.
7339
+ Incomplete checks do not fail automatically. A passing automated scan does not
7340
+ replace keyboard, screen-reader, focus-order or contrast review. See the
7341
+ [axe API](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md) for scan
7342
+ semantics. Run at both 390 and 1280 pixels with `assertViewport()` as well.
7343
+
7344
+ ## Watched development checks
7345
+
7346
+ Use an owned app copy with `app.env: { NODE_ENV: "development" }` and
7347
+ `app.start: [process.execPath, "node_modules/@warlock.js/core/bin/warlock.js", "dev"]`.
7348
+ Do not attach to an existing server. The fixture's `directory` is available in
7349
+ `run`; use `node:fs/promises` to edit only files inside that copy.
7350
+
7351
+ Wait for a visible revision marker after each edit; a successful file write alone
7352
+ does not prove watcher invalidation or browser hydration. Exercise several edits,
7353
+ including setup-file creation, changes and removal. After each revision, request
7354
+ the page again, exercise a hydrated control, check imported `shared` in loaders
7355
+ and middleware, and verify concurrent requests keep distinct values. Do not
7356
+ allowlist request-store or hydration errors to make these checks pass.
7357
+
7358
+ For forms beneath layouts, compare server-rendered IDs with the hydrated hook
7359
+ values (for example expose the hook value in a click handler), check repeated
7360
+ instances, label `for` and ARIA references, then navigate away and back. Comparing
7361
+ DOM IDs alone can miss a mismatch: React may retain the server attribute while
7362
+ client handlers capture another ID. Capture evidence and screenshots at every
7363
+ revision. Do not replace React's `useId()` globally or hide hydration warnings.
7364
+
7035
7365
 
7036
7366
  ## test-fakes `@warlock.js/core/skills/test-fakes/SKILL.md`
7037
7367
 
@@ -7625,6 +7955,97 @@ This is fine for normal test flow. It bites when you're inside a transaction the
7625
7955
  - The `warlock-conventions` topic — module layout, where `tests/*.test.ts` files live.
7626
7956
  - The `write-cli-command` topic — `warlock add test` scaffolds both global + worker setup files.
7627
7957
 
7958
+ ## Worker dictionaries and owned runtime resources
7959
+
7960
+ The generated `src/test-setup.ts` calls:
7961
+
7962
+ ```ts
7963
+ await setupTest({ importModule: (file) => import(file) });
7964
+ ```
7965
+
7966
+ `setupTest()` reads `src/app/*/utils/locales.json` after configuration in each
7967
+ Vitest worker. Server translation helpers therefore see all configured locales
7968
+ without manual registration. Invalid dictionaries fail setup. `teardownTest()`
7969
+ unregisters those dictionaries; legacy executable locale files still require
7970
+ explicit imports in worker setup.
7971
+
7972
+ Global HTTP fixtures should use `await startHttpTestServer({ port: 0, web: false })`
7973
+ for API-only suites and `await stopHttpTestServer()` in teardown. Stop releases
7974
+ connectors and the file runtime's loader channel, queued work, watchers, health
7975
+ workers, and registered module cleanup hooks, including after partial startup.
7976
+ A Node loader registration is process-scoped; its message channel is closed at
7977
+ teardown. Module-owned timers/listeners must register their cleanup hooks.
7978
+
7979
+ A module that owns a long-running handle can export an awaited cleanup:
7980
+
7981
+ ```ts
7982
+ // src/app/messages/main.ts
7983
+ import { createServer } from "node:net";
7984
+ const listener = createServer();
7985
+ listener.listen(0, "127.0.0.1");
7986
+ export async function cleanup(): Promise<void> {
7987
+ await new Promise<void>((resolve, reject) => {
7988
+ listener.close((error) => (error ? reject(error) : resolve()));
7989
+ });
7990
+ }
7991
+ ```
7992
+
7993
+ HTTP test teardown awaits these cleanup functions and reports failures after
7994
+ attempting every module's cleanup. Avoid leaving module-owned handles running
7995
+ outside this lifecycle.
7996
+
7997
+ ## Response contracts from declared schemas
7998
+
7999
+ `assertResponseContracts` from `@warlock.js/core/tests` sends HTTP requests and
8000
+ asserts the serialized JSON body against the handler's `responseSchema` for the
8001
+ actual status. It reuses Core's OpenAPI schema translation, including resource
8002
+ classes, nested objects, arrays, nullable casts and resource self references.
8003
+ It checks required fields and primitive types without coercion; extra fields
8004
+ are allowed. URL/date format annotations are not checked beyond their declared
8005
+ JSON shape. Builders, computed resolvers and resources without a static shape
8006
+ fail with a diagnostic rather than silently passing.
8007
+
8008
+ ```ts
8009
+ import { it } from "vitest";
8010
+ import { router } from "@warlock.js/core";
8011
+ import { assertResponseContracts } from "@warlock.js/core/tests";
8012
+
8013
+ it("matches product response declarations", async () => {
8014
+ await assertResponseContracts({
8015
+ routes: router.list().filter((route) => route.path === "/products"),
8016
+ cases: () => [{ path: "/products?page=1&limit=10", status: 200 }],
8017
+ requireAllStatuses: true,
8018
+ });
8019
+ });
8020
+ ```
8021
+
8022
+ With the shared test server, retain `startHttpTestServer({ port: 0 })` in global
8023
+ setup and `setupTest` in the worker. The routes and their handler schemas must
8024
+ also be loaded in the worker running this assertion (import their routes module
8025
+ there); global setup runs in a different process. Omit `routes` to discover all
8026
+ registered routes with `responseSchema` in that worker. Empty discovery fails.
8027
+ Explicit routes lacking a schema also fail.
8028
+
8029
+ Static GET routes default to one request without headers. Parameterized routes
8030
+ and non-GET methods require explicit `cases(route)` fixtures; an empty case
8031
+ list fails. Each case can supply a concrete `path`, `init` (headers, serialized
8032
+ body, etc.) and expected `status`. Supply fixtures with valid IDs, seeded data,
8033
+ authentication and invalid inputs to exercise the desired outcomes. An `all` route also needs a concrete fixture `method`. Redirects
8034
+ are not followed. POST/PUT/PATCH/DELETE fixtures execute real side effects;
8035
+ use isolated test data and clean it up.
8036
+
8037
+ Undeclared response statuses fail. Only statuses actually returned are checked
8038
+ by default; `requireAllStatuses: true` additionally fails if any declared status
8039
+ has no exercised fixture. HEAD, 204 and 304 responses must have an empty body; declare it with
8040
+ `{ body: {} }`. Other statuses must carry matching JSON.
8041
+
8042
+ For an in-process server, pass `client: app` from `createTestHttpServer()` and
8043
+ close it in teardown. For signed-in requests, pass `client: await actingAs(user)`
8044
+ and dispose that client afterward. These clients use the same assertion and
8045
+ run the real HTTP middleware and serialization paths.
8046
+
8047
+ 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.
8048
+
7628
8049
 
7629
8050
  ## test-service `@warlock.js/core/skills/test-service/SKILL.md`
7630
8051
 
@@ -7643,9 +8064,9 @@ For unit tests, you import the thing under test and call it directly. No HTTP, n
7643
8064
 
7644
8065
  ⛔ **If your project was generated before 4.14.0, fix BOTH the comment and the call.** The comment is false, **and** the generated `setupTest({ connectors: true })` is now an _explicit_ value that overrides your `src/config/tests.ts`. **Bare `setupTest()` is the correct call.**
7645
8066
 
7646
- **The lifetime is FILE-SCOPED, on purpose.** Your setup file bootstraps the framework and its `afterAll(teardownTest)` closes it, once per test file. **One owner, one pairing — correct under every pool, every isolation setting, and watch mode.**
8067
+ **The default lifetime is FILE-SCOPED.** Your setup file bootstraps the framework and its `afterAll(teardownTest)` closes it, once per test file. **One owner, one pairing — correct under every pool, every isolation setting, and watch mode.**
7647
8068
 
7648
- ⚠ **A worker-scoped lifetime is possible and is deliberately not shipped yet.** Lifecycle state now lives in the worker runtime, so leaving the framework running would let every file in a worker share one bootstrap. Two things block claiming it: under `pool: "threads"` we cannot observe whether Node reclaims a torn-down thread's sockets and pools, and **in watch mode Vitest reuses workers between reruns, so there is no recycle and no cleanup owner.** It gets taken when the real cost is measured and the integration is chosen, not inherited. See _Lifecycle and repeated calls_ below.
8069
+ Worker lifetime is available as an opt-in in 5.35.0. See the runtime lifetime section below for its runner, pool and isolation requirements.
7649
8070
 
7650
8071
  ⚠ **Changed in 4.13.0 — the import is a subpath now.** `setupTest` used to be re-exported from the package root; it is not any more, because that put the test helpers into every application's production module graph. `import { setupTest } from "@warlock.js/core"` now fails with _"has no exported member"_ — **add `/tests` to the specifier and nothing else changes.**
7651
8072
 
@@ -8047,7 +8468,7 @@ Node process, preserve the source package's module mode. ESM fixtures need a
8047
8468
  CommonJS and named exports fail before the behavior under test runs. Assert the
8048
8469
  child process exit status with its captured stderr before checking stdout.
8049
8470
 
8050
- - **`setupTest` runs per test file, not per worker** — every version of this skill through 4.13.0 said otherwise. **You pay one framework bootstrap per test file**, which is what 4.13.0 already cost; the difference is that it is now a chosen lifetime rather than a side effect of a module flag resetting.
8471
+ - **By default `setupTest` boots per test file** — every version of this skill through 4.13.0 said otherwise. **You pay one framework bootstrap per test file**, which is what 4.13.0 already cost; the difference is that it is now a chosen lifetime rather than a side effect of a module flag resetting.
8051
8472
  - ⛔ **Never delete the `afterAll(teardownTest)` from your setup file.** Without it nothing closes what `setupTest` opened, and the connectors outlive the file that started them.
8052
8473
  - ⛔ **You can't swap the connector set by calling `setupTest` again — it rejects now.** Through 4.13.0 the second call was silently ignored. Tear down first, or don't call it.
8053
8474
  - **These connections are separate from the HTTP test server's.** A row inserted by a service-level test is on this connection; the HTTP test server has its own. They only see each other if both point at the same physical DB **and** the inserting test has committed.
@@ -8075,6 +8496,164 @@ setupTest({ importModule: (file) => import(file) });
8075
8496
 
8076
8497
  The generated `src/test-setup.ts` already does this. Without it, `setupTest()` under Vitest warns once.
8077
8498
 
8499
+ ## Runtime lifetime, unit projects and setup profiling
8500
+
8501
+ `warlock add test` (also used by `create-warlock --features=test`) generates two
8502
+ Vitest projects. `unit` discovers `src/**/*.spec.ts` outside `tests/` directories
8503
+ and has no Warlock setup or HTTP global setup. `integration` discovers
8504
+ `src/**/*.test.ts`, specs inside `src/**/tests/`, and `tests/**/*.{test,spec}.ts`.
8505
+ It uses the framework setup, HTTP global setup and a generated `src/test-runner.ts`.
8506
+ Keep pure service specs beside their source and mock repositories at the boundary.
8507
+ Existing config files are preserved; copy the projects layout into them manually.
8508
+ Vitest 4.1.10 or newer is required for the generated runner.
8509
+
8510
+ For a unit spec that needs configuration values, use:
8511
+
8512
+ ```ts
8513
+ import { afterAll, expect } from "vitest";
8514
+ import { setupTest, teardownTest } from "@warlock.js/core/tests";
8515
+
8516
+ await setupTest({
8517
+ mode: "unit",
8518
+ importModule: (file) => import(file),
8519
+ file: expect.getState().testPath,
8520
+ });
8521
+ afterAll(teardownTest);
8522
+ ```
8523
+
8524
+ Unit mode loads `warlock.config.ts` and configuration values once per module graph.
8525
+ It skips bootstrap/env/prestart hooks, connectors, config special handlers,
8526
+ `filesOrchestrator.init()`, the native ESM loader, and fake installation. Keep
8527
+ configuration imports free of side effects; arbitrary code imported by a config
8528
+ file can still perform I/O. A plain unit spec usually needs no `setupTest()` at all.
8529
+ `connectors: false` still bootstraps; it is not the cheap unit mode. Use separate
8530
+ projects for unit and integration runtimes.
8531
+
8532
+ ### Opt into one runtime per fork
8533
+
8534
+ The default remains `tests.lifetime: "file"`. To reuse a runtime, change the
8535
+ integration project's `isolate` to `false`, keep `pool: "forks"` and
8536
+ `runner: "./src/test-runner.ts"`, then configure:
8537
+
8538
+ ```ts title="src/config/tests.ts"
8539
+ import { resetWorkerDatabase } from "../test-database";
8540
+
8541
+ export default {
8542
+ lifetime: "worker",
8543
+ // Application-owned callback: truncate only this worker's test database.
8544
+ // Called before every file's fixtures, including the first file.
8545
+ resetBetweenFiles: async () => {
8546
+ await resetWorkerDatabase();
8547
+ },
8548
+ profileSetup: true,
8549
+ };
8550
+ ```
8551
+
8552
+ Run with `npx vitest run --project integration`. Worker lifetime refuses watch,
8553
+ threads, isolated module graphs, or a missing runner with a message explaining
8554
+ the required settings. Keep `afterAll(teardownTest)` in the setup file: in worker
8555
+ mode it marks the end of the file; Vitest's awaited worker cleanup closes the
8556
+ runtime once. The runner uses `VitestTestRunner.onCleanupWorkerContext`, verified
8557
+ in Vitest 4.1.10's `runners.d.ts`, runner implementation and worker cleanup source.
8558
+ For an existing project, generate this runner without overwriting your config:
8559
+
8560
+ ```ts title="src/test-runner.ts"
8561
+ import { configureTestWorker } from "@warlock.js/core/tests";
8562
+ import { VitestTestRunner } from "vitest/runners";
8563
+
8564
+ export default class WarlockTestRunner extends VitestTestRunner {
8565
+ public constructor(config: ConstructorParameters<typeof VitestTestRunner>[0]) {
8566
+ super(config);
8567
+ configureTestWorker(
8568
+ { pool: this.pool, watch: config.watch, isolate: config.isolate },
8569
+ (cleanup) => this.onCleanupWorkerContext(cleanup),
8570
+ );
8571
+ }
8572
+ }
8573
+ ```
8574
+
8575
+ The first file bootstraps; subsequent files reuse connectors and config. Between
8576
+ files, selected fakes are restored and reinstalled, resetting mail/queue records,
8577
+ cache, secrets, clock, IDs and a private storage directory. Worker storage is
8578
+ separate from the global HTTP server's storage: use an in-process test HTTP server
8579
+ when assertions need to see request-side fake state. The hook runs after fake
8580
+ reset and before fixtures. It must finish before returning. Within a file,
8581
+ repeated `setupTest()` calls remain idempotent; use `beforeEach`/`afterEach` for
8582
+ per-test cleanup.
8583
+
8584
+ This trades module isolation for startup cost: application singletons, config
8585
+ mutations, mocks and module side effects can survive between files. Reset your
8586
+ own state in `resetBetweenFiles`, restore mocks, and keep connector selection
8587
+ consistent. Give each worker a distinct database/schema using
8588
+ `process.env.VITEST_POOL_ID` when preparing test connections. Never drop a shared
8589
+ database from this hook while other workers are using it. The framework does not
8590
+ create a memory replica set or a cross-worker lock; those belong to the application's
8591
+ global setup. A shared HTTP server and a single replica set can contend on I/O,
8592
+ locks and shared fixtures even though workers run concurrently. Scope rows and
8593
+ HTTP fixtures per worker, or run tests that require global mutation separately
8594
+ with `maxWorkers: 1`. No test-server teardown ownership changes are needed here.
8595
+
8596
+ `getTestRuntimeStats()` from `@warlock.js/core/tests` returns process-local
8597
+ `{ bootstraps, files }`. For example, after setup in worker mode:
8598
+
8599
+ ```ts
8600
+ import { expect, it } from "vitest";
8601
+ import { getTestRuntimeStats } from "@warlock.js/core/tests";
8602
+
8603
+ it("reuses this worker's runtime", () => {
8604
+ expect(getTestRuntimeStats().bootstraps).toBe(1);
8605
+ });
8606
+ ```
8607
+
8608
+ Across one integration project, a successful run with no worker recycling needs
8609
+ at most `maxWorkers` bootstraps. Multiple projects, worker crashes and memory-limit
8610
+ recycling can create additional processes; count distinct PIDs in profiles.
8611
+
8612
+ ### Measure and size CI workers
8613
+
8614
+ Set `tests.profileSetup: true` or run
8615
+ `WARLOCK_TEST_PROFILE=1 npx vitest run --project integration`. Pass
8616
+ `file: expect.getState().testPath` to `setupTest()` for file labels (the generated
8617
+ setup does this). Each file emits one line with milliseconds for environment, config,
8618
+ bootstrap, orchestrator, app config, connectors, fakes, reset and teardown as
8619
+ applicable. Reused files report reset instead of startup steps. A summary per
8620
+ worker reports file/bootstrap counts, summed step timings and final shutdown.
8621
+ The generated config also installs `TestSetupReporter` from `@warlock.js/core/vite`
8622
+ (`reporters: ["default", new TestSetupReporter()]`), which aggregates file counts,
8623
+ bootstrap counts and timings across PIDs into a run summary. Summed timings are
8624
+ not wall time. Worker shutdown is included when its summary reaches the reporter
8625
+ before the run-end callback; individual worker summaries always report it.
8626
+
8627
+ The generated config sets `maxWorkers: recommendedTestWorkers()` from
8628
+ `@warlock.js/core/vite`. It takes the smaller of CPU affinity and the tightest
8629
+ visible cgroup v2 quota, rounding fractional CPUs down with a minimum of one.
8630
+ For example `cpu.max = 1200000 100000` recommends 12 workers on a 48-CPU host.
8631
+ The format follows the [kernel cgroup v2 CPU interface](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#cpu-interface-files).
8632
+ It reads `/proc/self/cgroup` and visible ancestors under `/sys/fs/cgroup`; hosts
8633
+ without readable v2 quotas use affinity. Set `maxWorkers` explicitly on cgroup v1,
8634
+ custom cgroup mounts, or when database/memory limits require fewer workers.
8635
+
8636
+ ### Release gate
8637
+
8638
+ ```bash
8639
+ npx warlock test --changed --since "$LAST_RELEASE_REF"
8640
+ npx vitest run --project integration
8641
+ # Nightly (all projects):
8642
+ npx vitest run
8643
+ ```
8644
+
8645
+ The first command includes affected tests, using the existing changed-test CLI;
8646
+ the second always covers integration wiring. Keep the full suite nightly and
8647
+ compare cold and reused setup profiles on the same revision, worker count and
8648
+ database fixture before claiming a speedup.
8649
+
8650
+ For a manual fake harness outside `setupTest`, the same private storage behavior
8651
+ is available with `await installTestFakes(true, { isolatedStorage: true })`, paired
8652
+ with `await restoreTestFakes()`. Both are exported from `@warlock.js/core/tests`.
8653
+ `await storageFake.install({ isolated: true })` / `await storageFake.restore()`
8654
+ provides it for a storage-only harness. Existing installs share the HTTP root by
8655
+ default; the isolated option never replaces or deletes that shared root.
8656
+
8078
8657
 
8079
8658
  ## update-packages `@warlock.js/core/skills/update-packages/SKILL.md`
8080
8659
 
@@ -8763,6 +9342,24 @@ existing Oxc configs retain their categories. A real lockfile beside a workspace
8763
9342
  marker determines the installer: `package-lock.json` plus `pnpm-workspace.yaml`
8764
9343
  without `pnpm-lock.yaml` now produces `npm install`.
8765
9344
 
9345
+ ## 5.34 to 5.35 resource hidden input
9346
+
9347
+ Model resources now receive hidden input columns (subject to `resourceColumns`)
9348
+ to support derivations such as `has_secret: Boolean(data.secret)`. Model
9349
+ serialization removes `hidden` top-level keys from the final resource result.
9350
+ Review resources that copy input under renamed or nested keys and return explicit
9351
+ public projections. Direct resource construction does not invoke model filtering.
9352
+
9353
+ ## 5.34 to 5.35 document hydration
9354
+
9355
+ Initial Web hydration now includes `root.tsx`, matching the document ancestry
9356
+ used by SSR so `useId()` and generated Mongez form IDs agree beneath layouts.
9357
+ Root components and providers execute in both realms. Move server-only work into
9358
+ `root.setup.ts` loaders and pass browser-safe results as `data`; retain exactly
9359
+ one `#vessel` around `{children}`. Client navigation still swaps layout/page
9360
+ children while preserving the document root. Remove temporary explicit form IDs
9361
+ only when the application does not otherwise depend on those names.
9362
+
8766
9363
 
8767
9364
  ## upload-file `@warlock.js/core/skills/upload-file/SKILL.md`
8768
9365
 
@@ -9729,6 +10326,27 @@ This produces `account.title` and `account.actions.save`. Keep every leaf comple
9729
10326
 
9730
10327
  When a route-locales snapshot applies, `t()` (through the request binding) and `request.transFrom(locale, key, placeholders?)` resolve through it; without one, they retain the existing global-registry behavior. The underlying `transFromKeywords(locale, keywords, key, placeholders?, converter?)` is supplied by `@mongez/localization` 3.5.0. Error pages rebind to their own snapshot, while framework fallback uses the app/root snapshot. Development HMR add/edit/delete publishes a new snapshot, and the snapshot revision keeps page-cache entries from reusing old JSON. Generated `.warlock` typings contain the flattened route keys; do not maintain a second handwritten key union.
9731
10328
 
10329
+ ## Worker dictionaries and owned runtime resources
10330
+
10331
+ The generated `src/test-setup.ts` calls:
10332
+
10333
+ ```ts
10334
+ await setupTest({ importModule: (file) => import(file) });
10335
+ ```
10336
+
10337
+ `setupTest()` reads `src/app/*/utils/locales.json` after configuration in each
10338
+ Vitest worker. Server translation helpers therefore see all configured locales
10339
+ without manual registration. Invalid dictionaries fail setup. `teardownTest()`
10340
+ unregisters those dictionaries; legacy executable locale files still require
10341
+ explicit imports in worker setup.
10342
+
10343
+ Global HTTP fixtures should use `await startHttpTestServer({ port: 0, web: false })`
10344
+ for API-only suites and `await stopHttpTestServer()` in teardown. Stop releases
10345
+ connectors and the file runtime's loader channel, queued work, watchers, health
10346
+ workers, and registered module cleanup hooks, including after partial startup.
10347
+ A Node loader registration is process-scoped; its message channel is closed at
10348
+ teardown. Module-owned timers/listeners must register their cleanup hooks.
10349
+
9732
10350
 
9733
10351
  ## use-middleware `@warlock.js/core/skills/use-middleware/SKILL.md`
9734
10352
 
@@ -11530,7 +12148,7 @@ npx warlock typecheck --changed # changed files + the files importing them
11530
12148
  npx warlock typecheck --json --project tsconfig.app.json
11531
12149
  ```
11532
12150
 
11533
- `--changed` checks the changed files and their direct importers only; a full run (cheap after the first, it is incremental) catches anything further away.
12151
+ `--changed` checks diagnostics for changed files and their transitive importers. The native compiler still performs an incremental program compilation.
11534
12152
 
11535
12153
  ## `warlock explain`
11536
12154
 
@@ -11591,6 +12209,77 @@ Migrate lint and format tooling with
11591
12209
  `npx warlock upgrade --from 5.32 --to 5.33.0 --dry-run`, then remove `--dry-run`
11592
12210
  and work through the manual-review list. Use `--lint=oxc` for new apps.
11593
12211
 
12212
+ ## Web and response contract conventions
12213
+
12214
+ The following codes default to **warnings**, so upgrading does not fail existing projects.
12215
+ They parse real TypeScript/JSX syntax with the same oxc-parser adapter as the existing conventions.
12216
+
12217
+ | Code | Convention | Remedy |
12218
+ | -------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
12219
+ | `WL9013` | A component file exceeds 150 lines | Split components and extract hooks; configure `componentLineLimit`. |
12220
+ | `WL9014` | More than one component in a file | Move each component into its own TSX file. |
12221
+ | `WL9015` | State/effects or event-handler functions inside a component | Move `useState`, `useReducer`, `useEffect`, `useLayoutEffect` and handlers into `use-*.ts` hooks. |
12222
+ | `WL9016` | Native select, checkbox, radio, date, time or datetime-local input | Use the design-system control. Native controls are allowed in `src/web/design-system/**`. |
12223
+ | `WL9017` | DOM input/textarea/select native validation | Use shared schema validation instead of `required`, `pattern`, `min`, `max`, `minLength`, `maxLength` or input `type="email"`. Custom component props are allowed. |
12224
+ | `WL9018` | Exported `RequestHandler` in `src/app/**/controllers` lacks `responseSchema` | Assign `handler.responseSchema` to document each response status. |
12225
+
12226
+ Web rules apply to `src/web/**/*.tsx`. Components are top-level JSX-returning functions
12227
+ with a capitalized name (or an anonymous default export). Hook files and ordinary helpers
12228
+ are excluded. Handler checks recognize inline JSX callbacks, `handle*`/`on*` functions,
12229
+ and local function values referenced by event props. They do not execute code or infer
12230
+ runtime spreads, factory contracts or values imported from other files.
12231
+
12232
+ Create `.warlock-check.json` in the project root:
12233
+
12234
+ ```json
12235
+ {
12236
+ "componentLineLimit": 180,
12237
+ "rules": {
12238
+ "WL9013": "warning",
12239
+ "WL9015": "error",
12240
+ "WL9016": "off"
12241
+ }
12242
+ }
12243
+ ```
12244
+
12245
+ `rules` accepts `"off"`, `"warning"`, or `"error"` for convention codes, including
12246
+ existing codes. Per-line `// warlock-check-ignore WL9015 -- reason` works as before.
12247
+ Use `npx warlock explain WL9015` for the registry explanation.
12248
+
12249
+ ```bash
12250
+ npx warlock check --changed --fix --json
12251
+ ```
12252
+
12253
+ `--changed` selects tracked changes against HEAD plus untracked source files and their
12254
+ transitive import dependents (including re-exports, type imports and related tests).
12255
+ Resolution uses the project's compiler configuration, with relative/app imports as a
12256
+ fallback for files outside that configuration. Lint and conventions receive that selection;
12257
+ Vitest uses related mode. TypeScript still compiles incrementally as a program, then filters
12258
+ diagnostics to the selection and its importers; this is not a partial compilation.
12259
+
12260
+ `--fix` applies safe convention edits before the pipeline. Currently it removes `.js`
12261
+ suffixes from relative imports (`WL9002`). Ignored/disabled findings are not fixed.
12262
+ Component extraction, control replacement and response schemas require human decisions.
12263
+ Problems that have a safe edit include optional `fix: { start, end, text }` in JSON;
12264
+ `start` and `end` are zero-based source string offsets with an exclusive end.
12265
+ The report otherwise keeps schema version 1 and its existing shape. Applied fixes disappear
12266
+ from the final report; unresolved findings remain. Review the resulting source diff.
12267
+
12268
+ Programmatic usage: `await runCheck({ changed: true, fix: true })` from
12269
+ `@warlock.js/core/tooling`.
12270
+
12271
+ ## Explain a source file
12272
+
12273
+ `npx warlock explain src/app/posts/services/list.service.ts --json` reports the file's module, convention role, exports, imports, importers and reaching routes/pages. It boots the app for route/page registrations; error-code explanations remain static. See the `warlock-inspect` topic for graph semantics and the boot-free `npx warlock inspect --section files --json` command.
12274
+
12275
+ Starter controllers declare a response contract for each returned status. For example:
12276
+
12277
+ ```ts
12278
+ contactController.responseSchema = { 200: { body: { message: "string" } } };
12279
+ ```
12280
+
12281
+ Use `npx warlock inspect --section files --json` for static inspection without booting the app; `npx warlock explain src/app/contact/controllers/contact.controller.ts` explains its entry points.
12282
+
11594
12283
 
11595
12284
  ## warlock-conventions `@warlock.js/core/skills/warlock-conventions/SKILL.md`
11596
12285
 
@@ -12048,9 +12737,9 @@ npx warlock inspect --json > app.json # the full report
12048
12737
  npx warlock inspect --json --section routes,models # only some sections
12049
12738
  ```
12050
12739
 
12051
- It boots like `warlock doctor`: connectors boot (routes register) but none starts — no database, cache or socket connection, no port. With `--json`, stdout is only the JSON.
12740
+ Except for `--section files` (static source inspection without app boot), it boots like `warlock doctor`: connectors boot (routes register) but none starts — no database, cache or socket connection, no port. With `--json`, stdout is only the JSON.
12052
12741
 
12053
- Sections: `routes`, `models`, `resources`, `config`, `events`, `pages`, `jobs`, `schedules`, `packages`. An unknown section fails before the app boots.
12742
+ Sections: `routes`, `models`, `resources`, `config`, `events`, `pages`, `jobs`, `schedules`, `packages`, `files`. An unknown section fails before the app boots.
12054
12743
 
12055
12744
  ## Answer questions with jq
12056
12745
 
@@ -12168,6 +12857,49 @@ const valid = new Ajv2020({ strict: false }).validate(INSPECT_REPORT_JSON_SCHEMA
12168
12857
  - The `warlock-routes` topic — the human route table.
12169
12858
  - The `generate-openapi` topic — the same route schemas as an OpenAPI document.
12170
12859
 
12860
+ ## Session middleware in groups
12861
+
12862
+ `sessionMiddleware({ userTypes: "user" })` publishes the same `warlock.auth`
12863
+ descriptor as `authMiddleware()`. Group middleware is inherited by nested
12864
+ routes, so `inspect` and the generated `agents-md` summary recognize these guards.
12865
+ `{ optional: true }` reports `auth.required: false`, with sources and user types
12866
+ retained. Such routes remain in the unguarded-write list. Legacy descriptors
12867
+ without `required` still mean required authentication.
12868
+
12869
+ ```ts
12870
+ import { router } from "@warlock.js/core";
12871
+ import { sessionMiddleware } from "@warlock.js/auth";
12872
+
12873
+ router.group({ middleware: [sessionMiddleware({ userTypes: "user" })] }, () => {
12874
+ router.post("/settings", async ({ response }) => response.success({ saved: true }));
12875
+ });
12876
+ ```
12877
+
12878
+ ## Source files and reachability
12879
+
12880
+ ```bash
12881
+ npx warlock inspect --section files --json
12882
+ npx warlock explain src/app/posts/services/list.service.ts
12883
+ npx warlock explain src/web/components/Post.tsx --json
12884
+ npx warlock explain WL3001 --json
12885
+ ```
12886
+
12887
+ The `files` section reads every file under `src/`, without importing app modules or booting connectors. Each entry has `file`, `module`, `role`, `exports`, `imports` and `importedBy`. Paths are relative to the project root. Modules use `src/app/<module>` or the containing top-level `src` directory. Roles follow file names and directories: controller, service, repository, model, migration, schema, resource, routes, page, setup, layout, root, hook, component, util, type, config and spec; unmatched files use `source`. Non-code assets have empty exports and imports.
12888
+
12889
+ Each code file is parsed once with OXC per inspection. Export names include types, aliases, destructured bindings, `default` and `*` for star re-exports; star exports are recorded rather than expanded. Imports include type imports, side effects, re-exports and literal dynamic imports. Each import is `{ specifier, file }`: `file` is the resolved local path, or `null` for external/unresolved imports. Resolution follows inherited tsconfig path aliases, fallback targets, baseUrl, extensionless files and directory indexes within `src/`. Computed dynamic imports and CommonJS `require()` calls are not graph edges. Parse/config failures appear in `warnings`.
12890
+
12891
+ File explanations accept a project-relative or absolute path. They boot the app to read the existing routes/pages sections, then walk the static graph to find entry points reaching the file, including page layouts and loaders. Type-only edges participate; this is source reachability, not proof that runtime execution calls a function. Every route declared in a reaching route module is listed. Boot warnings remain in the JSON explanation and human output, so missing registrations are visible. `warlock explain <WLxxxx>` keeps its static error-code behavior.
12892
+
12893
+ ```ts
12894
+ import { inspectApp, explainFile } from "@warlock.js/core/tooling";
12895
+
12896
+ const report = await inspectApp({ sections: ["files"] });
12897
+ const source = explainFile("src/app/posts/services/list.service.ts", report);
12898
+ console.log(source.role, source.exports, source.importedBy);
12899
+ ```
12900
+
12901
+ For route/page reachability in code, call `inspectApp({ sections: ["files", "routes", "pages"] })` in an already booted app. `inspect_files` exposes the section through MCP with a case-insensitive `filter` on file, module or role.
12902
+
12171
12903
 
12172
12904
  ## warlock-mcp `@warlock.js/core/skills/warlock-mcp/SKILL.md`
12173
12905
 
@@ -12292,6 +13024,10 @@ await server.close();
12292
13024
  - The `warlock-trace` topic — send a request and read its timeline.
12293
13025
  - The `expose-as-mcp-server` topic of @warlock.js/ai-tools — the transport underneath.
12294
13026
 
13027
+ ## Source file graph
13028
+
13029
+ Call `inspect_files` with `{ "filter": "service" }` for file paths, modules, convention roles, export names, resolved imports and reverse `importedBy` edges. This section scans `src/` without executing source modules; files-only requests skip the lazy app boot. `inspect_app` includes a `files` count. See `npx warlock explain src/app/posts/services/list.service.ts --json` for reaching routes and pages.
13030
+
12295
13031
 
12296
13032
  ## warlock-routes `@warlock.js/core/skills/warlock-routes/SKILL.md`
12297
13033
 
@@ -13173,6 +13909,24 @@ label filters; never stop processes by command or name. `spawnPty()` lazily
13173
13909
  loads separately installed optional peer `node-pty`. The `warlock-js-process`
13174
13910
  skill contains complete working examples.
13175
13911
 
13912
+ ## Add the shared design system
13913
+
13914
+ ```bash
13915
+ npx warlock add tailwind
13916
+ npx warlock add design-system
13917
+ ```
13918
+
13919
+ `design-system` refuses before dependency installation when Tailwind is absent
13920
+ from `package.json` or `src/web/app.css`. It adds pinned shadcn primitives and
13921
+ Mongez controls to `src/web/design-system`, preserving existing source files.
13922
+ Use `--no-install` to record dependencies only, or `--dry-run --json` to preview
13923
+ files and dependencies without writing. For example, a generated control is
13924
+ `import TextInput from "web/design-system/components/text-input";` with
13925
+ `<TextInput id="profile-name" name="name" label="Name" />`.
13926
+ See Web's `build-accessible-controls` topic for the complete control contract.
13927
+
13928
+ Built-in `npx warlock add notifications` preloads `config: ["app"]` so generated locale stubs respect `app.localeCodes`, without bootstrap hooks or connectors.
13929
+
13176
13930
 
13177
13931
  ## write-middleware `@warlock.js/core/skills/write-middleware/SKILL.md`
13178
13932