@warlock.js/core 5.0.1 → 5.1.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 (120) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/esm/cli/cli-commands.manager.mjs +36 -1
  3. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  4. package/esm/cli/commands/dev-server.command.mjs +1 -1
  5. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs +207 -0
  6. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -0
  7. package/esm/cli/commands/doctor/checks/config.check.mjs +5 -2
  8. package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -1
  9. package/esm/cli/commands/doctor/checks/connectors.check.mjs +38 -12
  10. package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
  11. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +8 -4
  12. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
  13. package/esm/cli/commands/doctor/checks/health.check.mjs +77 -11
  14. package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
  15. package/esm/cli/commands/doctor/checks/index.mjs +5 -0
  16. package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
  17. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +116 -53
  18. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  19. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +27 -9
  20. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
  21. package/esm/cli/commands/doctor/checks/routes.check.mjs +51 -12
  22. package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
  23. package/esm/cli/commands/doctor/doctor-command.action.mjs +16 -6
  24. package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
  25. package/esm/cli/commands/doctor/run-checks.mjs +15 -6
  26. package/esm/cli/commands/doctor/run-checks.mjs.map +1 -1
  27. package/esm/cli/commands/doctor.command.mjs +17 -9
  28. package/esm/cli/commands/doctor.command.mjs.map +1 -1
  29. package/esm/connectors/describe-server-address.mjs +89 -6
  30. package/esm/connectors/describe-server-address.mjs.map +1 -1
  31. package/esm/connectors/http-connector.d.mts +18 -0
  32. package/esm/connectors/http-connector.d.mts.map +1 -1
  33. package/esm/connectors/http-connector.mjs +36 -3
  34. package/esm/connectors/http-connector.mjs.map +1 -1
  35. package/esm/dev-server/dev-logger.mjs +1 -4
  36. package/esm/dev-server/dev-logger.mjs.map +1 -1
  37. package/esm/dev-server/development-server.mjs +3 -7
  38. package/esm/dev-server/development-server.mjs.map +1 -1
  39. package/esm/dev-server/module-loader.mjs +1 -1
  40. package/esm/dev-server/ready-block.mjs +62 -0
  41. package/esm/dev-server/ready-block.mjs.map +1 -0
  42. package/esm/generations/add-command.action.mjs +2 -575
  43. package/esm/generations/add-command.action.mjs.map +1 -1
  44. package/esm/generations/features/access.feature.mjs +76 -0
  45. package/esm/generations/features/access.feature.mjs.map +1 -0
  46. package/esm/generations/features/ai-anthropic.feature.mjs +10 -0
  47. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -0
  48. package/esm/generations/features/ai-bedrock.feature.mjs +10 -0
  49. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -0
  50. package/esm/generations/features/ai-google.feature.mjs +10 -0
  51. package/esm/generations/features/ai-google.feature.mjs.map +1 -0
  52. package/esm/generations/features/ai-ollama.feature.mjs +10 -0
  53. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -0
  54. package/esm/generations/features/ai-openai.feature.mjs +10 -0
  55. package/esm/generations/features/ai-openai.feature.mjs.map +1 -0
  56. package/esm/generations/features/ai-panoptic.feature.mjs +13 -0
  57. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -0
  58. package/esm/generations/features/ai-tools.feature.mjs +13 -0
  59. package/esm/generations/features/ai-tools.feature.mjs.map +1 -0
  60. package/esm/generations/features/ai-workspace.feature.mjs +13 -0
  61. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -0
  62. package/esm/generations/features/ai.feature.mjs +15 -0
  63. package/esm/generations/features/ai.feature.mjs.map +1 -0
  64. package/esm/generations/features/herald.feature.mjs +19 -0
  65. package/esm/generations/features/herald.feature.mjs.map +1 -0
  66. package/esm/generations/features/image.feature.mjs +9 -0
  67. package/esm/generations/features/image.feature.mjs.map +1 -0
  68. package/esm/generations/features/index.mjs +72 -0
  69. package/esm/generations/features/index.mjs.map +1 -0
  70. package/esm/generations/features/mail.feature.mjs +10 -0
  71. package/esm/generations/features/mail.feature.mjs.map +1 -0
  72. package/esm/generations/features/mongodb.feature.mjs +9 -0
  73. package/esm/generations/features/mongodb.feature.mjs.map +1 -0
  74. package/esm/generations/features/mysql.feature.mjs +9 -0
  75. package/esm/generations/features/mysql.feature.mjs.map +1 -0
  76. package/esm/generations/features/notifications.feature.mjs +43 -0
  77. package/esm/generations/features/notifications.feature.mjs.map +1 -0
  78. package/esm/generations/features/postgres.feature.mjs +9 -0
  79. package/esm/generations/features/postgres.feature.mjs.map +1 -0
  80. package/esm/generations/features/react-email.feature.mjs +70 -0
  81. package/esm/generations/features/react-email.feature.mjs.map +1 -0
  82. package/esm/generations/features/react.feature.mjs +16 -0
  83. package/esm/generations/features/react.feature.mjs.map +1 -0
  84. package/esm/generations/features/redis.feature.mjs +9 -0
  85. package/esm/generations/features/redis.feature.mjs.map +1 -0
  86. package/esm/generations/features/s3.feature.mjs +13 -0
  87. package/esm/generations/features/s3.feature.mjs.map +1 -0
  88. package/esm/generations/features/scheduler.feature.mjs +9 -0
  89. package/esm/generations/features/scheduler.feature.mjs.map +1 -0
  90. package/esm/generations/features/ses.feature.mjs +9 -0
  91. package/esm/generations/features/ses.feature.mjs.map +1 -0
  92. package/esm/generations/features/shadcn.feature.mjs +397 -0
  93. package/esm/generations/features/shadcn.feature.mjs.map +1 -0
  94. package/esm/generations/features/shared/link-ai-package-import.mjs +37 -0
  95. package/esm/generations/features/shared/link-ai-package-import.mjs.map +1 -0
  96. package/esm/generations/features/shared/migration-timestamp.mjs +17 -0
  97. package/esm/generations/features/shared/migration-timestamp.mjs.map +1 -0
  98. package/esm/generations/features/socket.feature.mjs +15 -0
  99. package/esm/generations/features/socket.feature.mjs.map +1 -0
  100. package/esm/generations/features/tailwind.feature.mjs +174 -0
  101. package/esm/generations/features/tailwind.feature.mjs.map +1 -0
  102. package/esm/generations/features/test.feature.mjs +96 -0
  103. package/esm/generations/features/test.feature.mjs.map +1 -0
  104. package/esm/generations/features/web.feature.mjs +179 -0
  105. package/esm/generations/features/web.feature.mjs.map +1 -0
  106. package/esm/generations/stubs.mjs +95 -8
  107. package/esm/generations/stubs.mjs.map +1 -1
  108. package/esm/http/index.d.mts +1 -0
  109. package/esm/http/index.mjs +1 -0
  110. package/esm/http/ready-report.d.mts +56 -0
  111. package/esm/http/ready-report.d.mts.map +1 -0
  112. package/esm/http/ready-report.mjs +24 -0
  113. package/esm/http/ready-report.mjs.map +1 -0
  114. package/esm/index.d.mts +2 -1
  115. package/esm/index.mjs +2 -1
  116. package/llms-full.txt +322 -1
  117. package/llms.txt +2 -0
  118. package/package.json +12 -12
  119. package/skills/use-request-locals/SKILL.md +24 -1
  120. package/skills/warlock-conventions/SKILL.md +5 -1
package/llms-full.txt CHANGED
@@ -2833,6 +2833,152 @@ router.get(["/health", "/healthz"], healthController);
2833
2833
  - [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — module layout, the `guarded` convention, path aliases.
2834
2834
 
2835
2835
 
2836
+ ## request-memo `@warlock.js/core/request-memo/SKILL.md`
2837
+
2838
+ ---
2839
+ name: request-memo
2840
+ description: 'Memoize async work for one HTTP request with `requestMemo<T>(key, fn)` — the v5 replacement for removed `fromRequest`, with single-flight promise sharing, settled-success reuse, rejection eviction, and no cross-request fallback. Triggers: `requestMemo`, `fromRequest`, `fromRequest removed`, `request-scoped cache`, `single-flight`, `current request memo`; "migrate off fromRequest", "load this once per request", "deduplicate concurrent loaders", "cache a repository lookup during one request"; typical import `import { requestMemo } from "@warlock.js/core"`. Skip: middleware-written request state — `@warlock.js/core/use-request-locals/SKILL.md`; process-wide or cross-request caching — `@warlock.js/core/use-middleware/SKILL.md`; competing patterns: dynamic `request[key]` properties, module-global `Map`, payload `request.get()` / `request.set()`.'
2841
+ ---
2842
+
2843
+ # Warlock — memoize work for one request
2844
+
2845
+ `requestMemo()` is the v5 replacement for the removed `fromRequest()`. Use it when several code paths in the same HTTP request may need the same asynchronously computed value and the work should run once.
2846
+
2847
+ ## The exact signature
2848
+
2849
+ From `core/src/http/context/request-memo.ts`:
2850
+
2851
+ ```ts
2852
+ export function requestMemo<T>(key: string, fn: () => Promise<T>): Promise<T>
2853
+ ```
2854
+
2855
+ Import it from the package root:
2856
+
2857
+ ```ts
2858
+ import { requestMemo } from "@warlock.js/core";
2859
+ ```
2860
+
2861
+ `fn` takes no arguments and must return a promise. Close over the current request or the inputs needed by the loader.
2862
+
2863
+ ## Migrate from v4 to v5
2864
+
2865
+ ### Before — v4 `fromRequest()`
2866
+
2867
+ ```ts title="src/app/tenants/utils/current-tenant.ts"
2868
+ import { fromRequest, type Request } from "@warlock.js/core";
2869
+
2870
+ type Tenant = { id: string };
2871
+
2872
+ async function loadTenant(id: string): Promise<Tenant> {
2873
+ return { id };
2874
+ }
2875
+
2876
+ export function currentTenant(request: Request): Promise<Tenant> {
2877
+ const tenantId = String(request.header("x-tenant-id") ?? "");
2878
+
2879
+ return fromRequest("tenants.current", () => loadTenant(tenantId));
2880
+ }
2881
+ ```
2882
+
2883
+ ### After — v5 `requestMemo()`
2884
+
2885
+ ```ts title="src/app/tenants/utils/current-tenant.ts"
2886
+ import { requestMemo, type Request } from "@warlock.js/core";
2887
+
2888
+ type Tenant = { id: string };
2889
+
2890
+ async function loadTenant(id: string): Promise<Tenant> {
2891
+ return { id };
2892
+ }
2893
+
2894
+ export function currentTenant(request: Request): Promise<Tenant> {
2895
+ const tenantId = String(request.header("x-tenant-id") ?? "");
2896
+
2897
+ return requestMemo("tenants.current", () => loadTenant(tenantId));
2898
+ }
2899
+ ```
2900
+
2901
+ The call-site migration is usually the import and function name. The complete v5 example above also shows the callback-shape migration: if a v4 callback used the `Request` argument supplied by `fromRequest`, capture the surrounding `request` variable because `requestMemo`'s `fn` receives no arguments.
2902
+
2903
+ ## Single-flight behavior
2904
+
2905
+ Within one request, the first call for a key invokes `fn` and stores its promise. Every concurrent caller using that key receives that exact promise, so parallel consumers do not duplicate the work. After the promise resolves, later calls during the same request keep receiving the cached successful promise.
2906
+
2907
+ ```ts title="src/app/catalog/controllers/show-product.controller.ts"
2908
+ import { requestMemo, type RequestHandler } from "@warlock.js/core";
2909
+
2910
+ let loads = 0;
2911
+
2912
+ async function loadProduct(): Promise<{ id: string }> {
2913
+ loads += 1;
2914
+ return { id: "product-1" };
2915
+ }
2916
+
2917
+ export const showProductController: RequestHandler = async ({ response }) => {
2918
+ const first = requestMemo("catalog.product", loadProduct);
2919
+ const second = requestMemo("catalog.product", loadProduct);
2920
+ const [product, sameProduct] = await Promise.all([first, second]);
2921
+
2922
+ return response.success({ product, sameProduct, loads });
2923
+ };
2924
+ ```
2925
+
2926
+ For that request, `first === second` and `loads` is `1`.
2927
+
2928
+ A rejection is not cached. The rejected entry is deleted immediately, so a later call with the same key retries `fn`. Callers already sharing the rejected promise all observe that rejection.
2929
+
2930
+ ## Lifetime and isolation
2931
+
2932
+ The memo table is attached indirectly to the current request-context store through a `WeakMap`. Each incoming request gets a fresh store, so:
2933
+
2934
+ - two concurrent requests never share entries, even when they use the same key;
2935
+ - resolved values live only for the request's context lifetime;
2936
+ - the store and its memo entries are eligible for garbage collection after the request ends;
2937
+ - no value is written onto the `Request` object.
2938
+
2939
+ Calling `requestMemo()` outside the HTTP request pipeline throws synchronously. It deliberately has no process-global fallback. For data meant to survive across requests, use an explicit application cache instead.
2940
+
2941
+ ## Why it replaces `fromRequest()`
2942
+
2943
+ `fromRequest()` memoized through dynamic `request[key]` properties. That only type-checked because v4's `Request` had a `[key: string]: any` index signature. v5 removed both the unsafe index signature and `fromRequest()`; `requestMemo()` keeps the one-request lifetime without mutating `Request` or turning values into `any`.
2944
+
2945
+ The new helper also provides real single-flight behavior: it stores the in-flight promise. The old helper awaited its callback before writing the result onto `Request`, so concurrent v4 callers could run the same callback more than once.
2946
+
2947
+ ## Key design
2948
+
2949
+ Keys share one string namespace within a request. Use stable, namespaced keys and include every input that changes the result:
2950
+
2951
+ ```ts
2952
+ import { requestMemo } from "@warlock.js/core";
2953
+
2954
+ type Product = { id: string };
2955
+
2956
+ async function findProduct(id: string): Promise<Product> {
2957
+ return { id };
2958
+ }
2959
+
2960
+ export function productById(id: string): Promise<Product> {
2961
+ return requestMemo(`catalog.product:${id}`, () => findProduct(id));
2962
+ }
2963
+ ```
2964
+
2965
+ Reusing one key for different result types is a caller bug: TypeScript cannot compare the generic type arguments used at separate call sites.
2966
+
2967
+ ## Gotchas
2968
+
2969
+ - **Do not call it during bootstrap, in a CLI command, or from a background job.** There is no active HTTP request context there, so it throws.
2970
+ - **Do not omit result-changing inputs from the key.** `catalog.product` is wrong when different IDs may be loaded during one request; use `catalog.product:${id}`.
2971
+ - **Do not use `request.set()` as a replacement.** It writes to the request input payload read by `all()`, `input()`, and `validated()`.
2972
+ - **Failures retry.** If repeated failures must also be cached, this primitive is not that policy; catch and convert the failure into a successful result deliberately, or use another cache.
2973
+ - **Use `request.locals` for state written by middleware and read downstream.** Memoization and request attachment are separate jobs.
2974
+
2975
+ ## See also
2976
+
2977
+ - [`use-request-locals/SKILL.md`](../use-request-locals/SKILL.md) — typed per-request data written by middleware.
2978
+ - [`write-middleware/SKILL.md`](../write-middleware/SKILL.md) — authoring and registering middleware.
2979
+ - [`create-controller/SKILL.md`](../create-controller/SKILL.md) — consuming request-scoped values from controllers.
2980
+
2981
+
2836
2982
  ## resolve-path `@warlock.js/core/resolve-path/SKILL.md`
2837
2983
 
2838
2984
  ---
@@ -7232,6 +7378,177 @@ The third arg is a context object — `{ allValues, dateFormat, ... }` — for f
7232
7378
  - [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — controller → service → repository → model layering.
7233
7379
 
7234
7380
 
7381
+ ## use-request-locals `@warlock.js/core/use-request-locals/SKILL.md`
7382
+
7383
+ ---
7384
+ name: use-request-locals
7385
+ description: 'Carry typed, server-only data through one HTTP request with `request.locals`, usually written by middleware and read by downstream middleware or controllers. Augment `RequestLocals` in the module that owns each key; v5 no longer permits arbitrary `request.foo` properties. Triggers: `request.locals`, `RequestLocals`, `request.post`, `request.organization`, `Property does not exist on type Request`, `Request index signature`; "attach data to a request", "share middleware data with a controller", "type request locals", "migrate dynamic request properties"; typical type augmentation `declare module "@warlock.js/core" { interface RequestLocals { ... } }`. Skip: computed-on-demand single-flight values and removed `fromRequest` — `@warlock.js/core/request-memo/SKILL.md`; middleware mechanics — `@warlock.js/core/write-middleware/SKILL.md`; authenticated user typing — augment `RequestUser`, not `RequestLocals`; competing patterns: `(request as any).foo`, `request.set()`, module-global mutable state.'
7386
+ ---
7387
+
7388
+ # Warlock — use typed request locals
7389
+
7390
+ `request.locals` is the v5 home for private data that middleware writes and downstream code reads during the same request. It is separate from client input and starts as a fresh object on every `Request` instance.
7391
+
7392
+ ## The exact declarations
7393
+
7394
+ From `core/src/http/types.ts`:
7395
+
7396
+ ```ts
7397
+ export interface RequestLocals {}
7398
+ ```
7399
+
7400
+ From `core/src/http/request.ts`:
7401
+
7402
+ ```ts
7403
+ public locals: RequestLocals = {};
7404
+ ```
7405
+
7406
+ `RequestLocals` is intentionally empty and augmentable. It has no index signature: declare owned keys explicitly so reads and writes have their real types instead of `any`.
7407
+
7408
+ ## Migrate a middleware attachment from v4 to v5
7409
+
7410
+ ### Before — v4 dynamic `Request` property
7411
+
7412
+ ```ts title="src/app/observability/middleware/request-timing.middleware.ts"
7413
+ import type { Middleware } from "@warlock.js/core";
7414
+
7415
+ export const requestTimingMiddleware: Middleware = request => {
7416
+ request.startedAt = Date.now();
7417
+ };
7418
+ ```
7419
+
7420
+ ```ts title="src/app/observability/controllers/timing.controller.ts"
7421
+ import type { RequestHandler } from "@warlock.js/core";
7422
+
7423
+ export const timingController: RequestHandler = async (request, response) => {
7424
+ return response.success({ elapsedMs: Date.now() - request.startedAt });
7425
+ };
7426
+ ```
7427
+
7428
+ Those arbitrary properties compiled in v4 because `Request` had a `[key: string]: any` index signature. That signature is gone in v5.
7429
+
7430
+ ### After — v5 typed `request.locals`
7431
+
7432
+ Declare the key once in the feature that owns and writes it:
7433
+
7434
+ ```ts title="src/app/observability/request-locals.d.ts"
7435
+ declare module "@warlock.js/core" {
7436
+ interface RequestLocals {
7437
+ startedAt?: number;
7438
+ }
7439
+ }
7440
+
7441
+ export {};
7442
+ ```
7443
+
7444
+ Write it in middleware:
7445
+
7446
+ ```ts title="src/app/observability/middleware/request-timing.middleware.ts"
7447
+ import type { Middleware } from "@warlock.js/core";
7448
+
7449
+ export const requestTimingMiddleware: Middleware = ({ request }) => {
7450
+ request.locals.startedAt = Date.now();
7451
+ };
7452
+ ```
7453
+
7454
+ Read it downstream:
7455
+
7456
+ ```ts title="src/app/observability/controllers/timing.controller.ts"
7457
+ import type { RequestHandler } from "@warlock.js/core";
7458
+
7459
+ export const timingController: RequestHandler = async ({ request, response }) => {
7460
+ const startedAt = request.locals.startedAt;
7461
+
7462
+ if (startedAt === undefined) {
7463
+ return response.serverError({ error: "request timing middleware did not run" });
7464
+ }
7465
+
7466
+ return response.success({ elapsedMs: Date.now() - startedAt });
7467
+ };
7468
+ ```
7469
+
7470
+ The `?` is honest about runtime ordering: the type exists everywhere, but the value exists only after the middleware runs. Narrow it downstream unless every construction path guarantees initialization and your project deliberately declares the key as required.
7471
+
7472
+ ## Augment with an application type
7473
+
7474
+ An augmentation file may import the value's type. Keeping the declaration beside the writer makes ownership visible and prevents unrelated packages from claiming the same key.
7475
+
7476
+ ```ts title="src/app/organizations/request-locals.d.ts"
7477
+ import type { Organization } from "./models/organization";
7478
+
7479
+ declare module "@warlock.js/core" {
7480
+ interface RequestLocals {
7481
+ organization?: Organization;
7482
+ }
7483
+ }
7484
+
7485
+ export {};
7486
+ ```
7487
+
7488
+ The module specifier must be `"@warlock.js/core"`, matching the public package import whose `RequestLocals` interface is exported.
7489
+
7490
+ ## Where the augmentation file goes
7491
+
7492
+ A new project scaffolds `src/typings.d.ts` and lists it explicitly in `tsconfig.json`'s `include`. That is the sanctioned home for application-wide augmentations — `RequestLocals` and `RequestUser` both ship there as empty `interface` declarations with the reasoning written above them.
7493
+
7494
+ ```ts title="src/typings.d.ts"
7495
+ declare module "@warlock.js/core" {
7496
+ interface RequestLocals {}
7497
+
7498
+ interface RequestUser {}
7499
+ }
7500
+
7501
+ export {};
7502
+ ```
7503
+
7504
+ Feature-local files such as `src/app/organizations/request-locals.d.ts` are equally valid and keep ownership next to the middleware that writes the key; the scaffold's `include` covers all of `src`. Use `src/typings.d.ts` for declarations no single feature owns.
7505
+
7506
+ Two rules the scaffold's own comments spell out, and both bite silently:
7507
+
7508
+ - **Keep the trailing `export {}`.** `declare module "x"` inside a file with no top-level import or export declares an *ambient* module, which REPLACES `@warlock.js/core`'s real typings instead of merging into them — every framework export vanishes. The `export {}` is what makes the file a module and the block an augmentation. It is not an unused statement to clean up.
7509
+ - **Keep them `interface`, not `type`.** This project otherwise prefers `type`; these are the named exception, because declaration merging is interface-only. `type RequestUser = { ... }` is a duplicate-identifier error, not an augmentation.
7510
+
7511
+ On a project scaffolded before 5.1 there is no `src/typings.d.ts`, and `tsconfig.json` carries `"typeRoots": ["./src/typings.d.ts"]` — wrong twice, since `typeRoots` takes directories of `@types` packages rather than files, and that file did not exist. Drop the `typeRoots` entry, create the file, and list it under `include`.
7512
+
7513
+ ## What belongs in `locals`
7514
+
7515
+ Good fits are values explicitly produced by one stage and consumed by later stages:
7516
+
7517
+ - a resolved organization, session, feature flag, or authorization subject;
7518
+ - request timing or tracing metadata;
7519
+ - a model loaded by validation or routing middleware;
7520
+ - private server state that must not appear in request input.
7521
+
7522
+ `locals` is not a cache API. If consumers should be able to ask for a value independently and concurrent calls must collapse into one loader, use `requestMemo()` instead.
7523
+
7524
+ ## Isolation from input and other requests
7525
+
7526
+ `Request` initializes `locals` with `{}` for each new request. Request instances are not pooled, so values cannot carry into the next request.
7527
+
7528
+ Writing `request.locals.organization` does not affect `request.all()`, `request.input()`, or `request.validated()`. By contrast, `request.set()` writes into the input payload's `all` bag, so it is the wrong replacement for a private v4 attachment.
7529
+
7530
+ ## Choosing the extension surface
7531
+
7532
+ - Use `request.locals` for per-request data written by middleware and read downstream.
7533
+ - Use `requestMemo(key, fn)` for lazily computed, single-flight work scoped to the active request.
7534
+ - Augment `Request` itself only for a genuine new typed member that also has a runtime implementation.
7535
+ - Augment `RequestUser` for the authenticated `request.user` shape; do not duplicate it under locals merely to work around typing.
7536
+
7537
+ ## Gotchas
7538
+
7539
+ - **Do not restore `(request as any).post`.** It recreates the exact unchecked behavior v5 removed.
7540
+ - **Do not add `[key: string]: any` to `RequestLocals`.** Declare each key with its actual type; otherwise every typo becomes valid again.
7541
+ - **Do not centralize keys owned by unrelated features.** The middleware or package that writes a key should own its augmentation.
7542
+ - **Do not assume an optional local was initialized.** Middleware registration and ordering are runtime concerns; narrow the value or fail clearly downstream.
7543
+ - **Avoid key collisions across augmentations.** Interface merging requires repeated property declarations to agree, but stable feature-prefixed names are clearer for generic metadata.
7544
+
7545
+ ## See also
7546
+
7547
+ - [`request-memo/SKILL.md`](../request-memo/SKILL.md) — the `fromRequest()` migration and single-flight request memoization.
7548
+ - [`write-middleware/SKILL.md`](../write-middleware/SKILL.md) — middleware execution and registration.
7549
+ - [`create-controller/SKILL.md`](../create-controller/SKILL.md) — reading locals in downstream controllers.
7550
+
7551
+
7235
7552
  ## validate-input `@warlock.js/core/validate-input/SKILL.md`
7236
7553
 
7237
7554
  ---
@@ -7651,9 +7968,13 @@ import { Product } from "../../../products/models/product";
7651
7968
 
7652
7969
  Within the same module, use relative paths (`./`, `../`).
7653
7970
 
7971
+ ## Type augmentations
7972
+
7973
+ `src/typings.d.ts` is the project's module-augmentation file. It ships with the scaffold, is listed explicitly in `tsconfig.json`'s `include`, and is where application-wide `declare module "@warlock.js/core"` blocks live — `RequestLocals` and `RequestUser` are already there as empty interfaces. Keep its trailing `export {}` (without it the block declares an ambient module and erases the framework's real typings) and keep the augmented declarations as `interface`, the one sanctioned exception to this project's prefer-`type` rule, since declaration merging is interface-only. See [`use-request-locals`](../use-request-locals/SKILL.md).
7974
+
7654
7975
  ## Decorators
7655
7976
 
7656
- Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasMany` / `@MorphTo` for relations. The scaffolded `tsconfig.json` has `"experimentalDecorators": true`. If you ever see "model not registered" errors at runtime, check that flag first.
7977
+ Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasMany` / `@MorphTo` for relations. The scaffolded `tsconfig.json` has `"experimentalDecorators": false` these are native Stage-3 decorators, not legacy. Never set that flag to `true`; see [`lower-stage3-decorators`](../lower-stage3-decorators/SKILL.md).
7657
7978
 
7658
7979
  ## See also
7659
7980
 
package/llms.txt CHANGED
@@ -20,6 +20,7 @@
20
20
  - [lower-stage3-decorators](@warlock.js/core/lower-stage3-decorators/SKILL.md): Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won't load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core/vite"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.
21
21
  - [process-image](@warlock.js/core/process-image/SKILL.md): Transform images with the `Image` class — resize, crop, rotate, format, quality, watermark, blur, etc. — using a deferred pipeline that runs only at `save()` / `toBuffer()` / `toBase64()` / `toDataUrl()` time. Requires sharp via `warlock add image`. Triggers: `Image`, `Image.fromFile`, `Image.fromBuffer`, `Image.fromUrl`, `.resize`, `.crop`, `.watermark`, `.toBuffer`, `.toDataUrl`, `.apply`; "resize an image", "generate a thumbnail", "watermark a product photo", "build an image pipeline"; typical import `import { Image } from "@warlock.js/core"`. Skip: multipart upload entry — `@warlock.js/core/upload-file/SKILL.md`; storage persistence — `@warlock.js/core/store-file/SKILL.md`; competing libs `sharp` direct, `jimp`, `imagemagick`, `gm`.
22
22
  - [register-route](@warlock.js/core/register-route/SKILL.md): Register HTTP routes via @warlock.js/core's router — single routes, prefix groups, middleware-guarded blocks, and RESTful resource chains. Routes always live in `src/app/<module>/routes.ts`. Triggers: `router.get`, `router.post`, `router.prefix`, `router.group`, `router.route`, `guarded`; "add a route", "wire a controller to a URL", "group routes by prefix", "register a RESTful resource"; typical import `import { router } from "@warlock.js/core"`. Skip: handler shape — `@warlock.js/core/create-controller/SKILL.md`; CRUD chain details — `@warlock.js/core/build-restful/SKILL.md`; middleware authoring — `@warlock.js/core/write-middleware/SKILL.md`; competing libs `express`, `fastify`, `koa`, `@nestjs/common`.
23
+ - [request-memo](@warlock.js/core/request-memo/SKILL.md): Memoize async work for one HTTP request with `requestMemo<T>(key, fn)` — the v5 replacement for removed `fromRequest`, with single-flight promise sharing, settled-success reuse, rejection eviction, and no cross-request fallback. Triggers: `requestMemo`, `fromRequest`, `fromRequest removed`, `request-scoped cache`, `single-flight`, `current request memo`; "migrate off fromRequest", "load this once per request", "deduplicate concurrent loaders", "cache a repository lookup during one request"; typical import `import { requestMemo } from "@warlock.js/core"`. Skip: middleware-written request state — `@warlock.js/core/use-request-locals/SKILL.md`; process-wide or cross-request caching — `@warlock.js/core/use-middleware/SKILL.md`; competing patterns: dynamic `request[key]` properties, module-global `Map`, payload `request.get()` / `request.set()`.
23
24
  - [resolve-path](@warlock.js/core/resolve-path/SKILL.md): Path helpers anchored at `process.cwd()` — `rootPath`, `srcPath`, `appPath`, `configPath`, `publicPath`, `storagePath`, `uploadsPath`, `cachePath`, `logsPath`, `tempPath`, `warlockPath`, `sanitizePath`. Optional `uploads.root` config overrides the uploads anchor. Triggers: `appPath`, `configPath`, `uploadsPath`, `storagePath`, `publicPath`, `cachePath`, `logsPath`, `tempPath`, `sanitizePath`, `paths`; "resolve a path inside src/app", "absolute upload destination", "sanitize a user filename", "ship uploads to a mounted volume"; typical import `import { appPath, uploadsPath } from "@warlock.js/core"`. Skip: HTTP URL helpers — `@warlock.js/core/build-url/SKILL.md`; app metadata — `@warlock.js/core/use-app-context/SKILL.md`; storage abstraction — `@warlock.js/core/store-file/SKILL.md`; competing patterns: `path.join(process.cwd(), ...)`, hand-rolled directory constants.
24
25
  - [retry-operation](@warlock.js/core/retry-operation/SKILL.md): Wrap a flaky operation with `retry(fn, options)` — now provided by `@mongez/reinforcements` (not `@warlock.js/core`). `attempts` total tries, `delay` + `backoff` (linear/exponential/fn), `maxDelay`, `jitter`, `shouldRetry` to bail on permanent errors, `signal` to cancel, plus `retryable()` to pre-bind options. Triggers: `retry`, `retryable`, `RetryOptions`, `attempts`, `backoff`, `jitter`, `maxDelay`, `shouldRetry`, `signal`; "retry a flaky API call", "handle transient errors", "exponential backoff with jitter", "wrap an external request"; typical import `import { retry } from "@mongez/reinforcements"`. Skip: timing the retried op — `@warlock.js/core/benchmark-code/SKILL.md`; use-case-level `retry` option — `@warlock.js/core/write-use-case/SKILL.md`; competing libs `p-retry`, `async-retry`, `cockatiel`.
25
26
  - [run-app](@warlock.js/core/run-app/SKILL.md): Three operational commands — `warlock dev` (HMR + type-gen + health checks), `warlock build` (esbuild bundle), `warlock start` (spawn the production bundle). All flags, all `warlock.config.ts` knobs that shape them. Triggers: `warlock dev`, `warlock build`, `warlock start`, `devServer`, `--fresh`, `--skip-typings`, `--skip-health`, `outdir`, `outFile`, `sourcemap`; "start the dev server", "build for production", "run the bundle", "skip type generation", "tune watch globs", "dev server keyboard shortcuts", "press r to restart", "press q to quit", "restart the dev server"; typical config `warlock.config.ts > devServer / build`. Skip: writing a custom CLI — `@warlock.js/core/write-cli-command/SKILL.md`; config shape — `@warlock.js/core/configure-app/SKILL.md`; competing tooling `nodemon`, `tsx`, `ts-node-dev`, `esbuild` direct.
@@ -35,6 +36,7 @@
35
36
  - [use-middleware](@warlock.js/core/use-middleware/SKILL.md): Attach built-in HTTP middleware to routes via the `middleware` namespace from `@warlock.js/core` — rateLimit, concurrencyLimit, maxBodySize, idempotency, maintenance, ipFilter, cache. Plus `X-Request-Id` correlation, wired automatically. Triggers: `middleware.rateLimit`, `middleware.concurrencyLimit`, `middleware.maxBodySize`, `middleware.idempotency`, `middleware.maintenance`, `middleware.ipFilter`, `middleware.cache`, `X-Request-Id`, `Idempotency-Key`; "add rate limiting", "dedupe writes by idempotency key", "cap concurrent requests", "block IPs", "cache a GET response"; typical import `import { middleware } from "@warlock.js/core"`. Skip: author custom middleware — `@warlock.js/core/write-middleware/SKILL.md`; cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; competing libs `@fastify/rate-limit` direct, `express-rate-limit`, `helmet`.
36
37
  - [use-model-transformers](@warlock.js/core/use-model-transformers/SKILL.md): Three schema-side helpers — `useHashedPassword()` (bcrypt on save) attaches via `.addTransformer(...)`; `useComputedSlug(field?, scope?)` (auto-slug from another field) and `useComputedModel(callback)` (arbitrary computed-on-save value) attach via `v.computed(...)`. Triggers: `useHashedPassword`, `useComputedSlug`, `useComputedModel`, `.addTransformer`, `v.computed`, `ComputedCallback`; "auto-hash a password field", "auto-slug from title on save", "derive a value at write time", "declarative model transformers"; typical import `import { useHashedPassword, useComputedSlug } from "@warlock.js/core"`. Skip: bcrypt setup details — `@warlock.js/core/hash-password/SKILL.md`; repository writes — `@warlock.js/core/use-repository/SKILL.md`; output filtering — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: manual `await hashPassword(input)` in services, ORM lifecycle hooks.
37
38
  - [use-repository](@warlock.js/core/use-repository/SKILL.md): Subclass `RepositoryManager` for data access — declare `source`, `filterBy`, `defaultOptions`, then call `list()`/`listCached()`/`find()`/`create()`/`update()`/`delete()`, the active/cached/cursor variants, and the `filterBy`-aware aggregates `sum()`/`avg()`/`min()`/`max()`/`groupBy()`/`aggregate()`. Triggers: `RepositoryManager`, `FilterRules`, `RepositoryOptions`, `.list`, `.listCached`, `.find`, `.findCached`, `.create`, `.update`, `.delete`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.aggregate`, `simpleSelectColumns`; "create a repository", "filter rules for a list endpoint", "cursor vs page pagination", "cached vs uncached read", "sum/avg/group-by with filters"; typical import `import { RepositoryManager } from "@warlock.js/core"`. Skip: cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; use-case pipelines — `@warlock.js/core/write-use-case/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing libs `typeorm` Repository, `prisma.client.<model>`, `@nestjs/typeorm`.
39
+ - [use-request-locals](@warlock.js/core/use-request-locals/SKILL.md): Carry typed, server-only data through one HTTP request with `request.locals`, usually written by middleware and read by downstream middleware or controllers. Augment `RequestLocals` in the module that owns each key; v5 no longer permits arbitrary `request.foo` properties. Triggers: `request.locals`, `RequestLocals`, `request.post`, `request.organization`, `Property does not exist on type Request`, `Request index signature`; "attach data to a request", "share middleware data with a controller", "type request locals", "migrate dynamic request properties"; typical type augmentation `declare module "@warlock.js/core" { interface RequestLocals { ... } }`. Skip: computed-on-demand single-flight values and removed `fromRequest` — `@warlock.js/core/request-memo/SKILL.md`; middleware mechanics — `@warlock.js/core/write-middleware/SKILL.md`; authenticated user typing — augment `RequestUser`, not `RequestLocals`; competing patterns: `(request as any).foo`, `request.set()`, module-global mutable state.
38
40
  - [validate-input](@warlock.js/core/validate-input/SKILL.md): Author seal schemas, attach them to controllers via `controller.validation = { schema }`, infer types via `Infer<typeof schema>`, and layer DB-aware (`unique`/`exists`) and file validators on top. Triggers: `v.object`, `v.string`, `v.email`, `Infer`, `controller.validation`, `.unique`, `.exists`, `uniqueExceptCurrentId`, `request.validated`; "validate a request body", "attach a schema to a controller", "DB-aware unique rule", "infer schema types"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: schema authoring foundations — `@warlock.js/seal/seal-basics/SKILL.md`; controller wiring — `@warlock.js/core/create-controller/SKILL.md`; file rules deep-dive — `@warlock.js/core/upload-file/SKILL.md`; competing libs `zod`, `joi`, `yup`, `class-validator`.
39
41
  - [warlock-conventions](@warlock.js/core/warlock-conventions/SKILL.md): Framework-wide invariants for projects built on @warlock.js/core — module layout, canonical imports, layered flow, file naming, and the non-negotiable rules every other warlock skill assumes. Triggers: `src/app/<module>`, `routes.ts`, `main.ts`, `Request<TSchema>`, `RequestHandler`, `GuardedRequestHandler`, `app/<module>/...`; "where do files go in this project", "canonical Warlock imports", "module layout rules", "controller-service-repository layering"; typical import `import { router, type RequestHandler } from "@warlock.js/core"`. Skip: scaffold a new module — `@warlock.js/core/create-module/SKILL.md`; route shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing patterns: `express` ad-hoc layouts, `@nestjs/common` decorator-driven structure.
40
42
  - [warlock-doctor](@warlock.js/core/warlock-doctor/SKILL.md): Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `yarn warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.
package/package.json CHANGED
@@ -25,13 +25,13 @@
25
25
  "@mongez/slug": "^1.0.7",
26
26
  "@mongez/supportive-is": "^2.1.4",
27
27
  "@mongez/time-wizard": "^1.0.6",
28
- "@warlock.js/auth": "5.0.1",
29
- "@warlock.js/cache": "5.0.1",
30
- "@warlock.js/cascade": "5.0.1",
31
- "@warlock.js/context": "5.0.1",
32
- "@warlock.js/logger": "5.0.1",
33
- "@warlock.js/seal": "5.0.1",
34
- "@warlock.js/fs": "5.0.1",
28
+ "@warlock.js/auth": "5.1.0",
29
+ "@warlock.js/cache": "5.1.0",
30
+ "@warlock.js/cascade": "5.1.0",
31
+ "@warlock.js/context": "5.1.0",
32
+ "@warlock.js/logger": "5.1.0",
33
+ "@warlock.js/seal": "5.1.0",
34
+ "@warlock.js/fs": "5.1.0",
35
35
  "chokidar": "^5.0.0",
36
36
  "dayjs": "^1.11.19",
37
37
  "es-module-lexer": "^2.0.0",
@@ -57,10 +57,10 @@
57
57
  "react": "^19.2.3",
58
58
  "react-dom": "^19.2.3",
59
59
  "@react-email/render": "^2.0.5",
60
- "@warlock.js/herald": "5.0.1",
61
- "@warlock.js/ai": "5.0.1",
62
- "@warlock.js/access": "5.0.1",
63
- "@warlock.js/notifications": "5.0.1"
60
+ "@warlock.js/herald": "5.1.0",
61
+ "@warlock.js/ai": "5.1.0",
62
+ "@warlock.js/access": "5.1.0",
63
+ "@warlock.js/notifications": "5.1.0"
64
64
  },
65
65
  "peerDependenciesMeta": {
66
66
  "sharp": {
@@ -123,7 +123,7 @@
123
123
  ],
124
124
  "author": "hassanzohdy",
125
125
  "license": "MIT",
126
- "version": "5.0.1",
126
+ "version": "5.1.0",
127
127
  "type": "module",
128
128
  "main": "./esm/index.mjs",
129
129
  "module": "./esm/index.mjs",
@@ -103,7 +103,30 @@ declare module "@warlock.js/core" {
103
103
  export {};
104
104
  ```
105
105
 
106
- The module specifier must be `"@warlock.js/core"`, matching the public package import whose `RequestLocals` interface is exported. Ensure the `.d.ts` file is included by the application's TypeScript configuration.
106
+ The module specifier must be `"@warlock.js/core"`, matching the public package import whose `RequestLocals` interface is exported.
107
+
108
+ ## Where the augmentation file goes
109
+
110
+ A new project scaffolds `src/typings.d.ts` and lists it explicitly in `tsconfig.json`'s `include`. That is the sanctioned home for application-wide augmentations — `RequestLocals` and `RequestUser` both ship there as empty `interface` declarations with the reasoning written above them.
111
+
112
+ ```ts title="src/typings.d.ts"
113
+ declare module "@warlock.js/core" {
114
+ interface RequestLocals {}
115
+
116
+ interface RequestUser {}
117
+ }
118
+
119
+ export {};
120
+ ```
121
+
122
+ Feature-local files such as `src/app/organizations/request-locals.d.ts` are equally valid and keep ownership next to the middleware that writes the key; the scaffold's `include` covers all of `src`. Use `src/typings.d.ts` for declarations no single feature owns.
123
+
124
+ Two rules the scaffold's own comments spell out, and both bite silently:
125
+
126
+ - **Keep the trailing `export {}`.** `declare module "x"` inside a file with no top-level import or export declares an *ambient* module, which REPLACES `@warlock.js/core`'s real typings instead of merging into them — every framework export vanishes. The `export {}` is what makes the file a module and the block an augmentation. It is not an unused statement to clean up.
127
+ - **Keep them `interface`, not `type`.** This project otherwise prefers `type`; these are the named exception, because declaration merging is interface-only. `type RequestUser = { ... }` is a duplicate-identifier error, not an augmentation.
128
+
129
+ On a project scaffolded before 5.1 there is no `src/typings.d.ts`, and `tsconfig.json` carries `"typeRoots": ["./src/typings.d.ts"]` — wrong twice, since `typeRoots` takes directories of `@types` packages rather than files, and that file did not exist. Drop the `typeRoots` entry, create the file, and list it under `include`.
107
130
 
108
131
  ## What belongs in `locals`
109
132
 
@@ -114,9 +114,13 @@ import { Product } from "../../../products/models/product";
114
114
 
115
115
  Within the same module, use relative paths (`./`, `../`).
116
116
 
117
+ ## Type augmentations
118
+
119
+ `src/typings.d.ts` is the project's module-augmentation file. It ships with the scaffold, is listed explicitly in `tsconfig.json`'s `include`, and is where application-wide `declare module "@warlock.js/core"` blocks live — `RequestLocals` and `RequestUser` are already there as empty interfaces. Keep its trailing `export {}` (without it the block declares an ambient module and erases the framework's real typings) and keep the augmented declarations as `interface`, the one sanctioned exception to this project's prefer-`type` rule, since declaration merging is interface-only. See [`use-request-locals`](../use-request-locals/SKILL.md).
120
+
117
121
  ## Decorators
118
122
 
119
- Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasMany` / `@MorphTo` for relations. The scaffolded `tsconfig.json` has `"experimentalDecorators": true`. If you ever see "model not registered" errors at runtime, check that flag first.
123
+ Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasMany` / `@MorphTo` for relations. The scaffolded `tsconfig.json` has `"experimentalDecorators": false` these are native Stage-3 decorators, not legacy. Never set that flag to `true`; see [`lower-stage3-decorators`](../lower-stage3-decorators/SKILL.md).
120
124
 
121
125
  ## See also
122
126