@warlock.js/core 5.24.0 → 5.25.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 (203) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/bin/warlock.js +0 -0
  3. package/esm/application/application.d.mts +21 -0
  4. package/esm/application/application.d.mts.map +1 -1
  5. package/esm/application/application.mjs +29 -1
  6. package/esm/application/application.mjs.map +1 -1
  7. package/esm/application/boot-signal.d.mts +2 -1
  8. package/esm/application/boot-signal.d.mts.map +1 -1
  9. package/esm/application/boot-signal.mjs.map +1 -1
  10. package/esm/application/index.d.mts +1 -0
  11. package/esm/application/index.mjs +1 -0
  12. package/esm/application/roles.d.mts +72 -0
  13. package/esm/application/roles.d.mts.map +1 -0
  14. package/esm/application/roles.mjs +124 -0
  15. package/esm/application/roles.mjs.map +1 -0
  16. package/esm/cli/commands/add.command.mjs.map +1 -1
  17. package/esm/cli/commands/create-database.command.mjs.map +1 -1
  18. package/esm/cli/commands/dev-server.command.mjs.map +1 -1
  19. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
  20. package/esm/cli/commands/drop-tables.command.mjs.map +1 -1
  21. package/esm/cli/commands/generate/utils/path-resolver.mjs.map +1 -1
  22. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  23. package/esm/cli/commands/routes.command.mjs.map +1 -1
  24. package/esm/cli/commands/seed.command.mjs.map +1 -1
  25. package/esm/cli/commands/start-production.command.mjs +54 -5
  26. package/esm/cli/commands/start-production.command.mjs.map +1 -1
  27. package/esm/cli/commands/storage-put.action.mjs.map +1 -1
  28. package/esm/cli/commands/storage-put.command.mjs.map +1 -1
  29. package/esm/cli/commands/typings-generator.command.mjs +17 -1
  30. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  31. package/esm/cli/commands/update.command.mjs.map +1 -1
  32. package/esm/cli/commands-loader.mjs.map +1 -1
  33. package/esm/config/config-loader.d.mts.map +1 -1
  34. package/esm/config/config-loader.mjs +3 -1
  35. package/esm/config/config-loader.mjs.map +1 -1
  36. package/esm/connectors/connectors-manager.mjs.map +1 -1
  37. package/esm/connectors/database-connector.mjs.map +1 -1
  38. package/esm/connectors/herald-connector.mjs.map +1 -1
  39. package/esm/connectors/http-connector.d.mts.map +1 -1
  40. package/esm/connectors/http-connector.mjs +7 -0
  41. package/esm/connectors/http-connector.mjs.map +1 -1
  42. package/esm/connectors/logger-connector.mjs.map +1 -1
  43. package/esm/connectors/mail-connector.mjs.map +1 -1
  44. package/esm/connectors/types.d.mts +8 -0
  45. package/esm/connectors/types.d.mts.map +1 -1
  46. package/esm/connectors/types.mjs.map +1 -1
  47. package/esm/database/create-database-action.mjs.map +1 -1
  48. package/esm/database/drop-tables-action.mjs.map +1 -1
  49. package/esm/database/migrate-action.mjs +2 -1
  50. package/esm/database/migrate-action.mjs.map +1 -1
  51. package/esm/database/models/database-log/database-log.mjs.map +1 -1
  52. package/esm/database/seed-command-action.mjs +2 -1
  53. package/esm/database/seed-command-action.mjs.map +1 -1
  54. package/esm/dev-server/dependency-graph.mjs.map +1 -1
  55. package/esm/dev-server/development-server.mjs +2 -0
  56. package/esm/dev-server/development-server.mjs.map +1 -1
  57. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  58. package/esm/dev-server/layer-executor.mjs.map +1 -1
  59. package/esm/dev-server/manifest-manager.mjs.map +1 -1
  60. package/esm/dev-server/module-loader.mjs +10 -5
  61. package/esm/dev-server/module-loader.mjs.map +1 -1
  62. package/esm/dev-server/register-installed-devtools.mjs +47 -0
  63. package/esm/dev-server/register-installed-devtools.mjs.map +1 -0
  64. package/esm/dev-server/run-connector-typings.mjs +28 -0
  65. package/esm/dev-server/run-connector-typings.mjs.map +1 -0
  66. package/esm/dev-server/special-file-patterns.mjs +3 -1
  67. package/esm/dev-server/special-file-patterns.mjs.map +1 -1
  68. package/esm/dev-server/special-files-collector.d.mts +2 -2
  69. package/esm/dev-server/special-files-collector.d.mts.map +1 -1
  70. package/esm/dev-server/special-files-collector.mjs +7 -4
  71. package/esm/dev-server/special-files-collector.mjs.map +1 -1
  72. package/esm/encryption/password.mjs.map +1 -1
  73. package/esm/generations/add-command.action.mjs +3 -1
  74. package/esm/generations/add-command.action.mjs.map +1 -1
  75. package/esm/generations/features/access.feature.mjs.map +1 -1
  76. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
  77. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
  78. package/esm/generations/features/ai-google.feature.mjs.map +1 -1
  79. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
  80. package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
  81. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
  82. package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
  83. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
  84. package/esm/generations/features/ai.feature.mjs.map +1 -1
  85. package/esm/generations/features/devtools.feature.mjs +23 -0
  86. package/esm/generations/features/devtools.feature.mjs.map +1 -0
  87. package/esm/generations/features/herald.feature.mjs.map +1 -1
  88. package/esm/generations/features/image.feature.mjs.map +1 -1
  89. package/esm/generations/features/index.mjs +2 -0
  90. package/esm/generations/features/index.mjs.map +1 -1
  91. package/esm/generations/features/mail.feature.mjs.map +1 -1
  92. package/esm/generations/features/mongodb.feature.mjs.map +1 -1
  93. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  94. package/esm/generations/features/postgres.feature.mjs.map +1 -1
  95. package/esm/generations/features/react-email.feature.mjs.map +1 -1
  96. package/esm/generations/features/react.feature.mjs.map +1 -1
  97. package/esm/generations/features/redis.feature.mjs.map +1 -1
  98. package/esm/generations/features/s3.feature.mjs.map +1 -1
  99. package/esm/generations/features/scheduler.feature.mjs.map +1 -1
  100. package/esm/generations/features/ses.feature.mjs.map +1 -1
  101. package/esm/generations/features/socket.feature.mjs.map +1 -1
  102. package/esm/generations/features/test.feature.mjs +6 -2
  103. package/esm/generations/features/test.feature.mjs.map +1 -1
  104. package/esm/generations/features/types.mjs.map +1 -1
  105. package/esm/generations/features/web.feature.mjs.map +1 -1
  106. package/esm/http/database/RequestLog.mjs.map +1 -1
  107. package/esm/http/index.d.mts +2 -1
  108. package/esm/http/index.mjs +2 -1
  109. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  110. package/esm/http/middleware/inject-request-context.mjs +9 -1
  111. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  112. package/esm/http/response.d.mts +5 -5
  113. package/esm/http/response.d.mts.map +1 -1
  114. package/esm/http/response.mjs.map +1 -1
  115. package/esm/http/server-rebuilt.d.mts +20 -0
  116. package/esm/http/server-rebuilt.d.mts.map +1 -0
  117. package/esm/http/server-rebuilt.mjs +21 -0
  118. package/esm/http/server-rebuilt.mjs.map +1 -0
  119. package/esm/http/tracing/index.mjs +1 -1
  120. package/esm/http/tracing/tracing-dispatcher.d.mts +13 -6
  121. package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -1
  122. package/esm/http/tracing/tracing-dispatcher.mjs +46 -14
  123. package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -1
  124. package/esm/http/tracing/tracing.type.d.mts +2 -1
  125. package/esm/http/tracing/tracing.type.d.mts.map +1 -1
  126. package/esm/http/types.d.mts +9 -0
  127. package/esm/http/types.d.mts.map +1 -1
  128. package/esm/image/image.mjs.map +1 -1
  129. package/esm/index.d.mts +4 -2
  130. package/esm/index.mjs +4 -2
  131. package/esm/loader/app-module-importer.d.mts +17 -0
  132. package/esm/loader/app-module-importer.d.mts.map +1 -0
  133. package/esm/loader/app-module-importer.mjs +20 -0
  134. package/esm/loader/app-module-importer.mjs.map +1 -0
  135. package/esm/logger/logger.mjs.map +1 -1
  136. package/esm/mail/events.d.mts +7 -0
  137. package/esm/mail/events.d.mts.map +1 -1
  138. package/esm/mail/events.mjs +7 -0
  139. package/esm/mail/events.mjs.map +1 -1
  140. package/esm/mail/send-mail.d.mts.map +1 -1
  141. package/esm/mail/send-mail.mjs +23 -2
  142. package/esm/mail/send-mail.mjs.map +1 -1
  143. package/esm/mail/types.d.mts +5 -1
  144. package/esm/mail/types.d.mts.map +1 -1
  145. package/esm/mail/types.mjs.map +1 -1
  146. package/esm/manifest/manifest-manager.mjs.map +1 -1
  147. package/esm/production/build-contributions.mjs +19 -3
  148. package/esm/production/build-contributions.mjs.map +1 -1
  149. package/esm/production/production-builder.mjs +28 -8
  150. package/esm/production/production-builder.mjs.map +1 -1
  151. package/esm/production/production-supervisor.mjs +6 -0
  152. package/esm/production/production-supervisor.mjs.map +1 -1
  153. package/esm/production/resolve-build-config.d.mts +1 -1
  154. package/esm/production/resolve-build-config.mjs +15 -0
  155. package/esm/production/resolve-build-config.mjs.map +1 -1
  156. package/esm/production/route-registration-child.mjs +14 -2
  157. package/esm/production/route-registration-child.mjs.map +1 -1
  158. package/esm/production/route-registration-snapshot.mjs +9 -2
  159. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  160. package/esm/resource/resource-field-builder.mjs +1 -1
  161. package/esm/resource/resource-field-builder.mjs.map +1 -1
  162. package/esm/resource/resource.d.mts +5 -0
  163. package/esm/resource/resource.d.mts.map +1 -1
  164. package/esm/resource/resource.mjs +17 -6
  165. package/esm/resource/resource.mjs.map +1 -1
  166. package/esm/router/router.d.mts.map +1 -1
  167. package/esm/router/router.mjs +1 -0
  168. package/esm/router/router.mjs.map +1 -1
  169. package/esm/router/types.d.mts +2 -1
  170. package/esm/router/types.d.mts.map +1 -1
  171. package/esm/tests/index.d.mts +3 -2
  172. package/esm/tests/index.mjs +3 -2
  173. package/esm/tests/vitest-setup.d.mts +16 -1
  174. package/esm/tests/vitest-setup.d.mts.map +1 -1
  175. package/esm/tests/vitest-setup.mjs +20 -2
  176. package/esm/tests/vitest-setup.mjs.map +1 -1
  177. package/esm/use-cases/use-case-events.mjs.map +1 -1
  178. package/esm/utils/is-test-file.mjs +15 -0
  179. package/esm/utils/is-test-file.mjs.map +1 -0
  180. package/esm/utils/normalized-path.d.mts +3 -1
  181. package/esm/utils/normalized-path.d.mts.map +1 -1
  182. package/esm/utils/normalized-path.mjs +4 -2
  183. package/esm/utils/normalized-path.mjs.map +1 -1
  184. package/esm/utils/paths.mjs.map +1 -1
  185. package/esm/validation/plugins/database-plugin.mjs.map +1 -1
  186. package/esm/validation/validateAll.mjs +9 -3
  187. package/esm/validation/validateAll.mjs.map +1 -1
  188. package/esm/warlock-config/default-configurations.mjs +1 -0
  189. package/esm/warlock-config/default-configurations.mjs.map +1 -1
  190. package/esm/warlock-config/types.d.mts +7 -0
  191. package/esm/warlock-config/types.d.mts.map +1 -1
  192. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  193. package/llms-full.txt +264 -139
  194. package/package.json +15 -11
  195. package/skills/configure-app/SKILL.md +2 -0
  196. package/skills/define-resource/SKILL.md +4 -0
  197. package/skills/request-tracing/SKILL.md +38 -15
  198. package/skills/run-app/SKILL.md +71 -34
  199. package/skills/send-mail/SKILL.md +55 -25
  200. package/skills/test-http/SKILL.md +19 -8
  201. package/skills/test-service/SKILL.md +61 -40
  202. package/skills/warlock-conventions/SKILL.md +26 -18
  203. package/skills/write-cli-command/SKILL.md +50 -45
@@ -11,13 +11,13 @@ For unit tests, you import the thing under test and call it directly. No HTTP, n
11
11
 
12
12
  ⚠ **Corrected in 4.14.0 — `setupTest` is CALLED once per TEST FILE, not once per worker.** Every version of this skill through 4.13.0 said "per worker", the generated `src/test-setup.ts` carries a `Per-Worker Test Setup` comment saying the same thing, and **both were wrong.** Vitest runs `setupFiles` before **each test file**, and the setup module's registry is rebuilt every time — measured across all four `pool` × `isolate` combinations.
13
13
 
14
- ⛔ **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.**
14
+ ⛔ **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.**
15
15
 
16
16
  **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.**
17
17
 
18
- ⚠ **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.
18
+ ⚠ **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.
19
19
 
20
- ⚠ **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.**
20
+ ⚠ **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.**
21
21
 
22
22
  ## The shape
23
23
 
@@ -34,7 +34,7 @@ describe("registerUserService", () => {
34
34
  });
35
35
 
36
36
  expect(user.get("email")).toBe("test@example.com");
37
- expect(user.get("password")).not.toBe("secret"); // hashed by useHashedPassword()
37
+ expect(user.get("password")).not.toBe("secret"); // hashed by useHashedPassword()
38
38
 
39
39
  const found = await usersRepository.first({ email: "test@example.com" });
40
40
  expect(found).toBeDefined();
@@ -59,18 +59,18 @@ What it does (in order):
59
59
  3. Runs `bootstrap()` — env, app, prestart hooks.
60
60
  4. Initializes the `filesOrchestrator` (module/route/config discovery, no file watching).
61
61
  5. Loads all `src/config/*.ts` files.
62
- 6. Resolves the connector selection — **an explicit parameter wins, then `tests.connectors` from config, then the `true` default.** See *Selecting connectors* below.
62
+ 6. Resolves the connector selection — **an explicit parameter wins, then `tests.connectors` from config, then the `true` default.** See _Selecting connectors_ below.
63
63
  7. Starts the chosen connectors — but **never `http`** when you pass a boolean. HTTP is the global-setup's job.
64
64
 
65
65
  The result: DB/cache/logger/storage connections your code can use. Models save, repositories query, services run. Same code as production, just isolated to the test process.
66
66
 
67
67
  ### The `connectors` parameter
68
68
 
69
- | Value | Boots | Use when |
70
- | ---------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- |
71
- | `true` *(default)* | Every connector except `http` — via `startWithout(["http"])` (db, cache, logger, storage, and socket if configured) | Most service / repository / model tests. |
72
- | `false` | None | Pure logic tests with no DB / cache touches (parsers, validators, util functions). |
73
- | `["database", "cache"]` | Just those, in that order | A test that only needs DB but not, say, the storage driver. |
69
+ | Value | Boots | Use when |
70
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
71
+ | `true` _(default)_ | Every connector except `http` — via `startWithout(["http"])` (db, cache, logger, storage, and socket if configured) | Most service / repository / model tests. |
72
+ | `false` | None | Pure logic tests with no DB / cache touches (parsers, validators, util functions). |
73
+ | `["database", "cache"]` | Just those, in that order | A test that only needs DB but not, say, the storage driver. |
74
74
 
75
75
  The default `true` is the sane choice. Reach for `false` when the unit you're testing genuinely doesn't talk to any framework subsystem — pulling up a DB connection **for every file** just to test a string parser is wasted setup time.
76
76
 
@@ -86,25 +86,25 @@ export default testsConfigurations;
86
86
 
87
87
  ⚠ **BREAKING in 4.14.0 — the precedence flipped.**
88
88
 
89
- | | Order |
90
- |---|---|
91
- | **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true` |
92
- | **4.14.0 onward** | **explicit `setupTest({ connectors })` parameter** **>** `tests.connectors` config **>** `true` |
89
+ | | Order |
90
+ | ---------------------- | ----------------------------------------------------------------------------------------------- |
91
+ | **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true` |
92
+ | **4.14.0 onward** | **explicit `setupTest({ connectors })` parameter** **>** `tests.connectors` config **>** `true` |
93
93
 
94
- **An explicit call-site value now beats project config.** If your project sets `tests.connectors` *and* some test file passes `connectors` explicitly, **that file will start a different connector set after upgrading.** Search for `setupTest({` across your tests before you upgrade — a call passing `connectors` was previously ignored and is now honoured.
94
+ **An explicit call-site value now beats project config.** If your project sets `tests.connectors` _and_ some test file passes `connectors` explicitly, **that file will start a different connector set after upgrading.** Search for `setupTest({` across your tests before you upgrade — a call passing `connectors` was previously ignored and is now honoured.
95
95
 
96
96
  **"Explicit" means you supplied a non-`undefined` value.** Both of these fall through to config:
97
97
 
98
98
  ```ts
99
- await setupTest(); // → tests.connectors, else true
100
- await setupTest({}); // → tests.connectors, else true
99
+ await setupTest(); // → tests.connectors, else true
100
+ await setupTest({}); // → tests.connectors, else true
101
101
  await setupTest({ connectors: undefined }); // → tests.connectors, else true — NOT "start none"
102
102
  ```
103
103
 
104
104
  The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase your project config.
105
105
 
106
106
  ```ts
107
- await setupTest({ connectors: false }); // → none, even if config says otherwise
107
+ await setupTest({ connectors: false }); // → none, even if config says otherwise
108
108
  await setupTest({ connectors: ["database"] }); // → exactly that, even if config differs
109
109
  ```
110
110
 
@@ -131,7 +131,7 @@ afterAll(teardownTest);
131
131
  ⛔ **Three things changed here in 4.14.0. If you generated this file earlier, replace all three — it is not a comment fix.**
132
132
 
133
133
  1. **`afterAll(teardownTest)` is new and mandatory.** Nothing else closes the framework your tests started. This is what makes the lifetime file-scoped and owned rather than left running.
134
- 2. **The call is now bare `setupTest()`, not `setupTest({ connectors: true })`.** Under the flipped precedence, passing `true` is an *explicit* value and would override `tests.connectors` for **every file in the project.** Bare means "whatever this project configured, else the default".
134
+ 2. **The call is now bare `setupTest()`, not `setupTest({ connectors: true })`.** Under the flipped precedence, passing `true` is an _explicit_ value and would override `tests.connectors` for **every file in the project.** Bare means "whatever this project configured, else the default".
135
135
  3. **The comment used to say `Per-Worker Test Setup` / "Runs in EACH Vitest worker thread".** False — see the top of this skill.
136
136
 
137
137
  ```ts title="vite.config.ts"
@@ -142,8 +142,8 @@ import { defineConfig } from "vitest/config";
142
142
  export default defineConfig({
143
143
  plugins: [lowerStage3Decorators(), mongezVite()],
144
144
  test: {
145
- globalSetup: "./src/test-global-setup.ts", // ← HTTP server (see test-http skill)
146
- setupFiles: ["./src/test-setup.ts"], // ← runs setupTest before EACH test file
145
+ globalSetup: "./src/test-global-setup.ts", // ← HTTP server (see test-http skill)
146
+ setupFiles: ["./src/test-setup.ts"], // ← runs setupTest before EACH test file
147
147
  environment: "node",
148
148
  globals: false,
149
149
  include: ["src/app/**/*.test.ts"],
@@ -153,6 +153,17 @@ export default defineConfig({
153
153
 
154
154
  The `mongezVite()` plugin handles TypeScript path resolution and the framework's module shape. Without it, your imports break the moment vitest tries to load a Warlock module. `lowerStage3Decorators()` goes **first** — it lets decorated Cascade models (`@RegisterModel`, …) load under Vitest 4 / Vite 8; see [`@warlock.js/core/lower-stage3-decorators/SKILL.md`](@warlock.js/core/lower-stage3-decorators/SKILL.md).
155
155
 
156
+ ### Keep Vitest setup out of development
157
+
158
+ `warlock dev` (and `warlock build`) load your app's `vite.config.ts`. Anything you put there for tests runs in development too. Guard vitest-only setup:
159
+
160
+ ```ts
161
+ // vite.config.ts
162
+ if (process.env.VITEST) {
163
+ Object.assign(process.env, testEnv); // test database, test keys...
164
+ }
165
+ ```
166
+
156
167
  Tests live colocated with the module: `src/app/<module>/tests/*.test.ts`. The `include` pattern picks them up.
157
168
 
158
169
  ## Patterns
@@ -178,9 +189,9 @@ describe("createProductService", () => {
178
189
  it("rejects duplicate names", async () => {
179
190
  await Product.create({ name: "Existing", price: 10 });
180
191
 
181
- await expect(
182
- createProductService({ name: "Existing", price: 20 }),
183
- ).rejects.toThrow(/already exists/i);
192
+ await expect(createProductService({ name: "Existing", price: 20 })).rejects.toThrow(
193
+ /already exists/i,
194
+ );
184
195
  });
185
196
  });
186
197
  ```
@@ -250,7 +261,7 @@ afterEach(async () => {
250
261
 
251
262
  Vitest runs the tests within one file sequentially, so an `afterEach` truncate gives each test a clean slate.
252
263
 
253
- ⚠ **Cross-*file* isolation is not solved by this.** Separate workers get separate **connections**, not separate **rows** — two files pointed at the same database see each other's committed data regardless of pool or worker count. **Truncate what your file wrote; don't assume the worker boundary did it for you.** Real data isolation (DB-per-worker, transaction-per-test) is a separate piece of work and is not in this release.
264
+ ⚠ **Cross-_file_ isolation is not solved by this.** Separate workers get separate **connections**, not separate **rows** — two files pointed at the same database see each other's committed data regardless of pool or worker count. **Truncate what your file wrote; don't assume the worker boundary did it for you.** Real data isolation (DB-per-worker, transaction-per-test) is a separate piece of work and is not in this release.
254
265
 
255
266
  ### Skipping connectors for pure logic tests
256
267
 
@@ -260,7 +271,7 @@ import { setupTest } from "@warlock.js/core/tests";
260
271
  import { slugify } from "../utils/slugify";
261
272
 
262
273
  beforeAll(async () => {
263
- await setupTest({ connectors: false }); // starts no connectors at all
274
+ await setupTest({ connectors: false }); // starts no connectors at all
264
275
  });
265
276
 
266
277
  describe("slugify", () => {
@@ -287,7 +298,7 @@ beforeAll(async () => {
287
298
  });
288
299
 
289
300
  afterAll(async () => {
290
- await teardownTest(); // ← REQUIRED, see below
301
+ await teardownTest(); // ← REQUIRED, see below
291
302
  });
292
303
  ```
293
304
 
@@ -308,16 +319,16 @@ afterAll(async () => {
308
319
 
309
320
  `setupTest` / `teardownTest` are a pair. **The harness that calls one owns calling the other in the same context.**
310
321
 
311
- | Call | Behaviour |
312
- |---|---|
313
- | `setupTest(x)` while idle | bootstraps |
314
- | `setupTest(x)` while already ready with the **same** effective options | no-op |
315
- | `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested |
316
- | two concurrent `setupTest(x)` calls | share one startup |
317
- | `setupTest` after a failed setup | allowed — a failed setup unwinds and returns to idle |
318
- | `teardownTest()` while idle | no-op |
319
- | two concurrent `teardownTest()` calls | share one shutdown |
320
- | `setupTest(y)` after a **successful** teardown | allowed, different options fine |
322
+ | Call | Behaviour |
323
+ | --------------------------------------------------------------------------- | ---------------------------------------------------- |
324
+ | `setupTest(x)` while idle | bootstraps |
325
+ | `setupTest(x)` while already ready with the **same** effective options | no-op |
326
+ | `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested |
327
+ | two concurrent `setupTest(x)` calls | share one startup |
328
+ | `setupTest` after a failed setup | allowed — a failed setup unwinds and returns to idle |
329
+ | `teardownTest()` while idle | no-op |
330
+ | two concurrent `teardownTest()` calls | share one shutdown |
331
+ | `setupTest(y)` after a **successful** teardown | allowed, different options fine |
321
332
 
322
333
  **"Same options" is compared by meaning, not by literal value** — connector arrays are deduplicated and compared as sets, so `["cache", "database"]` and `["database", "cache", "cache"]` are the same selection.
323
334
 
@@ -332,7 +343,7 @@ afterAll(async () => {
332
343
  ```ts title="src/config/tests.ts"
333
344
  const testsConfigurations = {
334
345
  connectors: ["database", "logger"],
335
- setupTimeout: 120000, // milliseconds — this is the default
346
+ setupTimeout: 120000, // milliseconds — this is the default
336
347
  };
337
348
 
338
349
  export default testsConfigurations;
@@ -351,9 +362,9 @@ in `src/config/tests.ts` — milliseconds, default 120000.
351
362
  1. **It bounds the setup ATTEMPT, not teardown separately.** `teardownTest()` awaits the same attempt, so it inherits the bound — **one timer, not two.** A second teardown-side deadline was tried and rejected: it re-introduced the unbounded re-entry this whole guard exists to remove.
352
363
  2. **Expiry poisons the lifecycle**, it does not return to `idle`. The attempt may have started connectors nobody can now account for, so pretending the runtime is clean would be worse than refusing.
353
364
  3. **⛔ An invalid `setupTimeout` throws, naming the bad value.** Zero, negative and non-numeric all fail loudly rather than falling back to the default — a silent fallback would hide a typo behind a working suite.
354
- 4. **The bound is measured from when the attempt started**, not from when config was read. `tests.setupTimeout` is only readable after `loadConfigFiles()`, which happens *inside* the window being bounded; re-arming naively would give you the default plus your configured value.
365
+ 4. **The bound is measured from when the attempt started**, not from when config was read. `tests.setupTimeout` is only readable after `loadConfigFiles()`, which happens _inside_ the window being bounded; re-arming naively would give you the default plus your configured value.
355
366
 
356
- ⚠ **What is proven and what is not.** The nine guards above were each seen to fail under their own mutation. **But every spec injects its scheduler**, so the default *value* is tested while the production timer — and whether its `unref` actually releases the worker — is not. **And no spec observes a real hang**: the stuck attempt is a mock gate, not a socket that never returns. These prove what the lifecycle *decides*, not what a genuinely wedged connector does.
367
+ ⚠ **What is proven and what is not.** The nine guards above were each seen to fail under their own mutation. **But every spec injects its scheduler**, so the default _value_ is tested while the production timer — and whether its `unref` actually releases the worker — is not. **And no spec observes a real hang**: the stuck attempt is a mock gate, not a socket that never returns. These prove what the lifecycle _decides_, not what a genuinely wedged connector does.
357
368
 
358
369
  ### State is per worker runtime, not per module
359
370
 
@@ -383,3 +394,13 @@ In both cases the guard's scope matches the resource's scope, which is the point
383
394
  - [`test-http/SKILL.md`](../test-http/SKILL.md) — integration tests via the real HTTP server (`startHttpTestServer` + `testGet` / `testPost` / `expectJson`).
384
395
  - [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — where tests live in a module (`tests/*.test.ts`).
385
396
  - [`write-cli-command/SKILL.md`](../write-cli-command/SKILL.md) — `warlock add test` for the initial scaffold.
397
+
398
+ ## App modules and `importModule`
399
+
400
+ Under Vitest, test files run through vite-node while Warlock's own native `import()` of app modules (config, routes, events) does not, so app classes such as `User` can exist twice in one worker and `instanceof` fails. Pass `importModule` to `setupTest` so Warlock loads app modules through the test's own module graph:
401
+
402
+ ```ts
403
+ setupTest({ importModule: (file) => import(file) });
404
+ ```
405
+
406
+ The generated `src/test-setup.ts` already does this. Without it, `setupTest()` under Vitest warns once.
@@ -13,6 +13,9 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
13
13
  2. **Several files are auto-loaded — never `import` them.** The dev-server file watcher and the production builder categorize these as "special" and load them on boot.
14
14
  - `src/app/<module>/routes.ts` — route declarations
15
15
  - `src/app/<module>/main.ts` — per-module one-time setup
16
+ - `src/app/<module>/worker.ts` — worker-only setup (queue job definitions,
17
+ `scheduler.addJob(...)`, `scheduler.start()`); loaded only for the
18
+ `worker` role, and always loaded by `warlock dev`
16
19
  - `src/app/main.ts` — **project-level** one-time application setup (global hooks and other app initialization; connector instances belong in `warlock.config.ts > connectors`)
17
20
  - `src/app/<module>/events/*.ts(x)` — **any** `.ts(x)` file inside the `events/` folder (the `*.event.ts` suffix is convention, not a framework requirement)
18
21
  - `src/app/<module>/utils/locales.ts` — module translations via `groupedTranslations(...)`
@@ -33,7 +36,7 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
33
36
  ```ts title="src/app/<module>/schema/create-<thing>.schema.ts"
34
37
  import { v, type Infer } from "@warlock.js/seal";
35
38
 
36
- export const createThingSchema = v.object({ /* … */ });
39
+ export const createThingSchema = v.object({/* … */});
37
40
  export type CreateThingSchema = Infer<typeof createThingSchema>;
38
41
  ```
39
42
 
@@ -42,13 +45,17 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
42
45
  import { type CreateThingSchema, createThingSchema } from "../schema/create-thing.schema";
43
46
  import { createThingService } from "../services/create-thing.service";
44
47
 
45
- export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({ request, response }) => {
48
+ export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({
49
+ request,
50
+ response,
51
+ }) => {
46
52
  const thing = await createThingService(request.validated());
47
53
  return response.success({ thing });
48
54
  };
49
55
 
50
56
  createThingController.validation = { schema: createThingSchema };
51
57
  ```
58
+
52
59
  8. **Model getters beat `.get<T>("field")`.** Add a typed getter on the model rather than scattering `.get<string>("name")` casts across call sites.
53
60
 
54
61
  ## Module layout (the standard subfolders)
@@ -67,26 +74,27 @@ src/app/<module>/
67
74
  seeds/ seed data for `warlock seed`
68
75
  routes.ts ← auto-loaded
69
76
  main.ts ← auto-loaded once, one-time setup
77
+ worker.ts ← auto-loaded for the worker role only
70
78
  ```
71
79
 
72
80
  Some older modules still have a `validation/` folder instead of `schema/` — historical drift. The framework treats them as plain folders either way; the generator emits to `schema/`.
73
81
 
74
82
  ## File naming (project-wide)
75
83
 
76
- | Suffix | Role | Example |
77
- | ------------------- | ------------------------------------- | ----------------------------------- |
78
- | `.controller.ts` | HTTP handler | `create-product.controller.ts` |
79
- | `.service.ts` | Stateless business logic | `create-product.service.ts` |
80
- | `.usecase.ts` | `useCase()` pipeline | `login.usecase.ts` |
81
- | `.model.ts` | Cascade model class | `product.model.ts` |
82
- | `.repository.ts` | `RepositoryManager` subclass | `products.repository.ts` |
83
- | `.resource.ts` | `Resource` subclass | `product.resource.ts` |
84
- | `.schema.ts` | seal validation schema + its inferred `<Name>Schema` type from the same file | `create-product.schema.ts` |
85
- | `.type.ts` | data shape (TypeScript `type`) | `cart-state.type.ts` |
86
- | `.contract.ts` | interface (TypeScript `interface`) | `model.contract.ts` |
87
- | `.event.ts` | event listener registrations (any `.ts(x)` inside `events/` works; the suffix is convention) | `audit.event.ts` |
88
- | `.migration.ts` | Cascade migration | `2026_05_22_120000_product.migration.ts` |
89
- | `.seed.ts` | seed data | `products.seed.ts` |
84
+ | Suffix | Role | Example |
85
+ | ---------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------- |
86
+ | `.controller.ts` | HTTP handler | `create-product.controller.ts` |
87
+ | `.service.ts` | Stateless business logic | `create-product.service.ts` |
88
+ | `.usecase.ts` | `useCase()` pipeline | `login.usecase.ts` |
89
+ | `.model.ts` | Cascade model class | `product.model.ts` |
90
+ | `.repository.ts` | `RepositoryManager` subclass | `products.repository.ts` |
91
+ | `.resource.ts` | `Resource` subclass | `product.resource.ts` |
92
+ | `.schema.ts` | seal validation schema + its inferred `<Name>Schema` type from the same file | `create-product.schema.ts` |
93
+ | `.type.ts` | data shape (TypeScript `type`) | `cart-state.type.ts` |
94
+ | `.contract.ts` | interface (TypeScript `interface`) | `model.contract.ts` |
95
+ | `.event.ts` | event listener registrations (any `.ts(x)` inside `events/` works; the suffix is convention) | `audit.event.ts` |
96
+ | `.migration.ts` | Cascade migration | `2026_05_22_120000_product.migration.ts` |
97
+ | `.seed.ts` | seed data | `products.seed.ts` |
90
98
 
91
99
  Filenames are kebab-case. Class names are PascalCase. Function names are camelCase. Table/collection names are snake_case (plural).
92
100
 
@@ -97,8 +105,8 @@ The scaffolded `tsconfig.json` defines:
97
105
  ```jsonc
98
106
  {
99
107
  "paths": {
100
- "app/*": ["./src/app/*"]
101
- }
108
+ "app/*": ["./src/app/*"],
109
+ },
102
110
  }
103
111
  ```
104
112
 
@@ -43,16 +43,16 @@ Run it: `npx warlock users.promote --email=hasan@example.com` (or `npx warlock u
43
43
 
44
44
  ## `CLICommandOptions` — the factory input
45
45
 
46
- | Field | Type | Required | Notes |
47
- | ------------- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
48
- | `name` | `string` | yes | Dot notation OK (`db.seed`, `jwt.generate`). May include positional placeholders (`name <arg>`). |
49
- | `description` | `string` | | Shown in `warlock --help` and `warlock <cmd> --help`. |
50
- | `alias` | `string` | | Short name (`m` for `migrate`). |
51
- | `action` | `(data) => void \| Promise` | yes | Runs after preloaders. `data` is `{ args, options }`. |
52
- | `preAction` | `(data) => void \| Promise` | | Runs **before** preloaders — banner, input validation. |
53
- | `preload` | `CLICommandPreload` | | What to load before `action` runs. See below. |
54
- | `persistent` | `boolean` | | `true` for long-running commands (dev server). Skips the auto-exit. |
55
- | `options` | `CLICommandOption[]` | | Flag definitions. See below. |
46
+ | Field | Type | Required | Notes |
47
+ | ------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
48
+ | `name` | `string` | yes | Dot notation OK (`db.seed`, `jwt.generate`). May include positional placeholders (`name <arg>`). |
49
+ | `description` | `string` | | Shown in `warlock --help` and `warlock <cmd> --help`. |
50
+ | `alias` | `string` | | Short name (`m` for `migrate`). |
51
+ | `action` | `(data) => void \| Promise` | yes | Runs after preloaders. `data` is `{ args, options }`. |
52
+ | `preAction` | `(data) => void \| Promise` | | Runs **before** preloaders — banner, input validation. |
53
+ | `preload` | `CLICommandPreload` | | What to load before `action` runs. See below. |
54
+ | `persistent` | `boolean` | | `true` for long-running commands (dev server). Skips the auto-exit. |
55
+ | `options` | `CLICommandOption[]` | | Flag definitions. See below. |
56
56
 
57
57
  ## Options — flag shape
58
58
 
@@ -154,29 +154,29 @@ The framework ships a fixed set of commands you call but don't author. Knowing t
154
154
 
155
155
  ### Database + migrations
156
156
 
157
- | Command | Flags / args | Preloads |
158
- | -------- | ----------------------------------------------- | ------------------------------ |
159
- | `warlock migrate` | `--list` / `-l` (executed **and** pending), `--pending` (pending only, sets an exit code), `--fresh` / `-f` (drop tables first) | database, logger |
160
- | `warlock seed` | `--name <pattern>` (run seeds matching the pattern) | full bootstrap (env, configs, app modules) |
161
- | `warlock create-database <name>` | bare positional `<name>` | database |
162
- | `warlock drop.tables` | `--force, -f` (skip confirmation prompt) | database, logger |
163
- | `warlock db.indexes` | builds DB indexes for every registered model | database |
157
+ | Command | Flags / args | Preloads |
158
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
159
+ | `warlock migrate` | `--list` / `-l` (executed **and** pending), `--pending` (pending only, sets an exit code), `--fresh` / `-f` (drop tables first) | database, logger |
160
+ | `warlock seed` | `--name <pattern>` (run seeds matching the pattern) | full bootstrap (env, configs, app modules) |
161
+ | `warlock create-database <name>` | bare positional `<name>` | database |
162
+ | `warlock drop.tables` | `--force, -f` (skip confirmation prompt) | database, logger |
163
+ | `warlock db.indexes` | builds DB indexes for every registered model | database |
164
164
 
165
165
  **Asking what will run next.** `warlock migrate --list` prints executed migrations and then the pending ones **in execution order**. Do not derive the pending set by differencing `--all` against `--list`: `--all` globs `src/app` only, while `--list` reads the migrations table, which also holds migrations that packages register through `database.migrations` (`@warlock.js/auth` contributes two). The difference under-counts pending, in the direction that says "safe to proceed".
166
166
 
167
167
  `--list` is a report and always exits `0`. `--pending` is a gate, and its exit code is its whole API:
168
168
 
169
- | Exit | Meaning |
170
- | ---- | ------- |
169
+ | Exit | Meaning |
170
+ | ---- | ------------------------- |
171
171
  | `0` | computed, nothing pending |
172
- | `1` | computed, N pending |
172
+ | `1` | computed, N pending |
173
173
  | `2` | **could not be computed** |
174
174
 
175
175
  ```bash
176
176
  warlock migrate --pending && ./deploy.sh
177
177
  ```
178
178
 
179
- `2` is separate from `1` on purpose — a script must be able to tell a backlog from an unknown, because one means *run them* and the other means *stop*. When the migration files cannot be loaded, both commands print `Pending: unavailable — <reason>` beneath a complete executed listing rather than reporting `0`.
179
+ `2` is separate from `1` on purpose — a script must be able to tell a backlog from an unknown, because one means _run them_ and the other means _stop_. When the migration files cannot be loaded, both commands print `Pending: unavailable — <reason>` beneath a complete executed listing rather than reporting `0`.
180
180
 
181
181
  **If you are writing a command that reports on pending migrations:** register migrations first. `listPendingMigrations()` filters the runner's registry, so a caller that has not loaded anything gets `[]` — which reads as "nothing pending" and is not the same claim.
182
182
 
@@ -202,36 +202,41 @@ warlock generate (alias: g) — interactive picker
202
202
 
203
203
  `warlock add <features...>` installs framework-adjacent features through a curated registry — bundles npm dependencies + scaffold files + tsconfig patches + package.json scripts in one call. Featured names (from `add-command.action.ts`):
204
204
 
205
- | Feature | Installs |
206
- | ------------- | --------------------------------------------------------------------------------------------------- |
207
- | `react-email` | `react-email` + `@react-email/components` + `@react-email/render` + `@react-email/tailwind`; drops a `welcome-email.tsx` sample; patches `tsconfig.json` |
208
- | `web` | Warlock Web + React stack; scaffolds the application page, localization, contact endpoint, and Web configuration |
209
- | `react` | `react` + `react-dom` + types |
205
+ | Feature | Installs |
206
+ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
207
+ | `react-email` | `react-email` + `@react-email/components` + `@react-email/render` + `@react-email/tailwind`; drops a `welcome-email.tsx` sample; patches `tsconfig.json` |
208
+ | `web` | Warlock Web + React stack; scaffolds the application page, localization, contact endpoint, and Web configuration |
209
+ | `react` | `react` + `react-dom` + types |
210
210
  | `bull-board` | `@bull-board/api` + `@bull-board/fastify`; writes a `dashboard` block to `src/config/queue.ts`. `requires: ["queue"]` — adds the `queue` feature first automatically when it's missing |
211
- | `image` | `sharp` (for the `Image` class) |
212
- | `mail` | `nodemailer` + types |
213
- | `ses` | `@aws-sdk/client-sesv2` |
214
- | `mongodb` | `mongodb` driver |
215
- | `postgres` | `pg` |
216
- | `mysql` | `mysql2` |
217
- | `redis` | `redis` |
218
- | `s3` | `@aws-sdk/client-s3` + `@aws-sdk/lib-storage` + `@aws-sdk/s3-request-presigner` |
219
- | `scheduler` | `@warlock.js/scheduler` |
220
- | `swagger` | `@warlock.js/swagger` |
221
- | `postman` | `@warlock.js/postman` |
222
- | `herald` | `@warlock.js/herald` + `amqplib` + types; ejects `src/config/communicator.ts` |
223
- | `test` | `vitest` + `@mongez/vite` + coverage; drops `test-global-setup.ts` / `test-setup.ts` / `vite.config.ts` |
211
+ | `image` | `sharp` (for the `Image` class) |
212
+ | `mail` | `nodemailer` + types |
213
+ | `ses` | `@aws-sdk/client-sesv2` |
214
+ | `mongodb` | `mongodb` driver |
215
+ | `postgres` | `pg` |
216
+ | `mysql` | `mysql2` |
217
+ | `redis` | `redis` |
218
+ | `s3` | `@aws-sdk/client-s3` + `@aws-sdk/lib-storage` + `@aws-sdk/s3-request-presigner` |
219
+ | `scheduler` | `@warlock.js/scheduler` |
220
+ | `swagger` | `@warlock.js/swagger` |
221
+ | `postman` | `@warlock.js/postman` |
222
+ | `herald` | `@warlock.js/herald` + `amqplib` + types; ejects `src/config/communicator.ts` |
223
+ | `test` | `vitest` + `@mongez/vite` + coverage; drops `test-global-setup.ts` / `test-setup.ts` / `vite.config.ts` |
224
+ | `devtools` | `@warlock.js/devtools` as a dev dependency; `warlock dev` auto-loads its development dashboard at `/__warlock` |
224
225
 
225
226
  Run `warlock add --list` to see what's currently registered. Pass `--packageManager <yarn\|pnpm\|npm>` to override auto-detection (defaults to whichever lockfile is present).
226
227
 
228
+ `warlock add devtools` never adds anything to `warlock.config.ts`. The package
229
+ is loaded automatically by `warlock dev` when installed, and is omitted from a
230
+ production install with dev dependencies excluded.
231
+
227
232
  The Web starter keeps the page's universal non-component setup in `src/web/index.register.ts`, while `src/web/index.page.tsx` re-exports `register`. Warlock still discovers and runs the hook from the page namespace in both SSR and the browser, but editing the page component remains inside React's Fast Refresh boundary. Do not move the `register()` declaration inline beside the component: React Refresh treats that non-component export as incompatible and reloads the page, losing component state.
228
233
 
229
234
  ### Misc
230
235
 
231
- | Command | Purpose |
232
- | -------- | ----------------------------------------------------------- |
233
- | `warlock storage.put <localPath> [destination]` | Upload a local file to the default storage disk |
234
- | `warlock jwt.generate` | Generate a JWT secret in `.env` (from `@warlock.js/auth`) |
236
+ | Command | Purpose |
237
+ | ----------------------------------------------- | --------------------------------------------------------- |
238
+ | `warlock storage.put <localPath> [destination]` | Upload a local file to the default storage disk |
239
+ | `warlock jwt.generate` | Generate a JWT secret in `.env` (from `@warlock.js/auth`) |
235
240
 
236
241
  ## Common patterns
237
242
 
@@ -293,7 +298,7 @@ export default command({
293
298
  });
294
299
  ```
295
300
 
296
- `preAction` runs *before* preloaders — cheap way to bail out without spinning up the database.
301
+ `preAction` runs _before_ preloaders — cheap way to bail out without spinning up the database.
297
302
 
298
303
  ## Gotchas
299
304
 
@@ -301,7 +306,7 @@ export default command({
301
306
  - **Don't put logic at module top level.** The file gets imported during the commands scan (`warlock --warm-cache`). Anything outside `action` runs at scan time, possibly before any config or env is loaded.
302
307
  - **Required options block execution.** If `required: true` and the user omits the flag, the framework prints `Missing required options:` and exits `1` before `action` runs.
303
308
  - **Connectors are not free.** `connectors: true` boots the database, cache, storage, etc. For a print-version-and-exit command, leave `preload` undefined.
304
- - **`name` field with positional slots** (`name: "storage.put <localPath>"`) — the *registered* name is still `storage.put` (the first whitespace-separated token), so look up + alias work normally. Slots are only documentation/help-output.
309
+ - **`name` field with positional slots** (`name: "storage.put <localPath>"`) — the _registered_ name is still `storage.put` (the first whitespace-separated token), so look up + alias work normally. Slots are only documentation/help-output.
305
310
  - **Aliases must be unique across plugin + framework + project.** The first registration wins; later collisions silently overwrite the map entry.
306
311
 
307
312
  ## See also