create-warlock 4.2.6 → 4.2.8

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 (95) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +2 -2
  3. package/templates/warlock/.env.example +36 -0
  4. package/templates/warlock/.gitattributes +1 -0
  5. package/templates/warlock/.husky/pre-commit +4 -0
  6. package/templates/warlock/.prettierignore +4 -0
  7. package/templates/warlock/.prettierrc.json +10 -0
  8. package/templates/warlock/.vscode/settings.json +41 -0
  9. package/templates/warlock/README.md +57 -0
  10. package/templates/warlock/_.gitignore +6 -0
  11. package/templates/warlock/docs/new-module.md +551 -0
  12. package/templates/warlock/eslint.config.js +98 -0
  13. package/templates/warlock/package.json +74 -0
  14. package/templates/warlock/public/home.css +523 -0
  15. package/templates/warlock/skills/api-design/SKILL.md +461 -0
  16. package/templates/warlock/skills/code-standards/SKILL.md +595 -0
  17. package/templates/warlock/skills/data-and-persistence/SKILL.md +330 -0
  18. package/templates/warlock/skills/git-workflow/SKILL.md +282 -0
  19. package/templates/warlock/skills/module-boundaries/SKILL.md +283 -0
  20. package/templates/warlock/skills/observability-and-resilience/SKILL.md +306 -0
  21. package/templates/warlock/skills/security-baseline/SKILL.md +352 -0
  22. package/templates/warlock/skills/testing-strategy/SKILL.md +323 -0
  23. package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +28 -0
  24. package/templates/warlock/src/app/auth/controllers/login.controller.ts +22 -0
  25. package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +16 -0
  26. package/templates/warlock/src/app/auth/controllers/logout.controller.ts +16 -0
  27. package/templates/warlock/src/app/auth/controllers/me.controller.ts +13 -0
  28. package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +29 -0
  29. package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +23 -0
  30. package/templates/warlock/src/app/auth/main.ts +9 -0
  31. package/templates/warlock/src/app/auth/models/otp/index.ts +1 -0
  32. package/templates/warlock/src/app/auth/models/otp/migrations/22-12-2025_10-30-20.otp-migration.ts +30 -0
  33. package/templates/warlock/src/app/auth/models/otp/otp.model.ts +69 -0
  34. package/templates/warlock/src/app/auth/requests/guarded.request.ts +10 -0
  35. package/templates/warlock/src/app/auth/routes.ts +22 -0
  36. package/templates/warlock/src/app/auth/schema/login.schema.ts +8 -0
  37. package/templates/warlock/src/app/auth/schema/reset-password.schema.ts +9 -0
  38. package/templates/warlock/src/app/auth/services/auth.service.ts +66 -0
  39. package/templates/warlock/src/app/auth/services/forgot-password.service.ts +28 -0
  40. package/templates/warlock/src/app/auth/services/otp.service.ts +173 -0
  41. package/templates/warlock/src/app/auth/services/reset-password.service.ts +39 -0
  42. package/templates/warlock/src/app/auth/utils/auth-error-code.ts +6 -0
  43. package/templates/warlock/src/app/auth/utils/locales.ts +89 -0
  44. package/templates/warlock/src/app/auth/utils/types.ts +14 -0
  45. package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +21 -0
  46. package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +30 -0
  47. package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +15 -0
  48. package/templates/warlock/src/app/posts/models/post/post.model.ts +23 -0
  49. package/templates/warlock/src/app/posts/resources/post.resource.ts +14 -0
  50. package/templates/warlock/src/app/posts/routes.ts +8 -0
  51. package/templates/warlock/src/app/posts/schema/create-post.schema.ts +9 -0
  52. package/templates/warlock/src/app/posts/schema/update-post.schema.ts +9 -0
  53. package/templates/warlock/src/app/shared/components/HomePageComponent.tsx +229 -0
  54. package/templates/warlock/src/app/shared/controllers/home-page.controller.ts +18 -0
  55. package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +8 -0
  56. package/templates/warlock/src/app/shared/routes.ts +4 -0
  57. package/templates/warlock/src/app/shared/services/scheduler.service.ts +3 -0
  58. package/templates/warlock/src/app/shared/tests/infrastructure.test.ts +22 -0
  59. package/templates/warlock/src/app/shared/utils/global-columns-schema.ts +8 -0
  60. package/templates/warlock/src/app/shared/utils/locales.ts +766 -0
  61. package/templates/warlock/src/app/shared/utils/router.ts +30 -0
  62. package/templates/warlock/src/app/uploads/controllers/fetch-uploaded-file.controller.ts +33 -0
  63. package/templates/warlock/src/app/uploads/routes.ts +4 -0
  64. package/templates/warlock/src/app/users/commands/hello-world.command.ts +8 -0
  65. package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +27 -0
  66. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +12 -0
  67. package/templates/warlock/src/app/users/events/inject-created-by-user.into-model.event.ts +32 -0
  68. package/templates/warlock/src/app/users/events/sync.ts +5 -0
  69. package/templates/warlock/src/app/users/main.ts +5 -0
  70. package/templates/warlock/src/app/users/models/user/index.ts +1 -0
  71. package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +15 -0
  72. package/templates/warlock/src/app/users/models/user/user.model.ts +64 -0
  73. package/templates/warlock/src/app/users/repositories/users.repository.ts +23 -0
  74. package/templates/warlock/src/app/users/resources/user.resource.ts +14 -0
  75. package/templates/warlock/src/app/users/routes.ts +8 -0
  76. package/templates/warlock/src/app/users/schema/create-user.schema.ts +11 -0
  77. package/templates/warlock/src/app/users/seeds/users.seed.ts +21 -0
  78. package/templates/warlock/src/app/users/services/get-users.service.ts +5 -0
  79. package/templates/warlock/src/app/users/services/list-users.service.ts +17 -0
  80. package/templates/warlock/src/app/users/services/login-social.ts +19 -0
  81. package/templates/warlock/src/config/app.ts +12 -0
  82. package/templates/warlock/src/config/auth.ts +20 -0
  83. package/templates/warlock/src/config/cache.ts +59 -0
  84. package/templates/warlock/src/config/database.ts +65 -0
  85. package/templates/warlock/src/config/http.ts +23 -0
  86. package/templates/warlock/src/config/log.ts +22 -0
  87. package/templates/warlock/src/config/mail.ts +16 -0
  88. package/templates/warlock/src/config/repository.ts +11 -0
  89. package/templates/warlock/src/config/storage.ts +34 -0
  90. package/templates/warlock/src/config/tests.ts +5 -0
  91. package/templates/warlock/src/config/validation.ts +7 -0
  92. package/templates/warlock/storage/.gitignore +2 -0
  93. package/templates/warlock/tsconfig.json +27 -0
  94. package/templates/warlock/warlock.config.ts +15 -0
  95. package/templates/warlock/yarn.lock +2332 -0
@@ -0,0 +1,595 @@
1
+ ---
2
+ name: code-standards
3
+ description: 'Project-level TypeScript code standards for apps built on Warlock.js — `interface` vs `type`, access modifiers, no `any`, no inline-duplicated unions, file-naming suffixes (`.contract.ts` / `.type.ts` / `.service.ts` / `.model.ts` / `.resource.ts` / `.spec.ts`), single-responsibility files, FP-factory + internal-class pattern, brace-every-control-block readability with blank lines between consecutive blocks, JSDoc on public surface and non-obvious logic, error classes extending framework errors, async/await + `Promise.all`, `undefined` over `null`, Vitest co-located, ESLint + Prettier. Triggers: writing/editing any `.ts` / `.tsx` file in this project; user asks "what are the code standards / style rules / conventions"; spawning a sub-agent that will write code; reviewing a diff for style issues; deciding `interface` vs `type`, when to use a class, where helpers live, how to name a file, or whether something needs JSDoc. Skip: pure-Markdown / JSON / YAML edits with no code change; runtime / behavior questions about a specific framework primitive — load the relevant `@warlock.js/<pkg>` skill instead; package-publishing or CI workflow questions.'
4
+ ---
5
+
6
+ # Code Standards
7
+
8
+ **Status:** Stable
9
+ **Applies to:** All TypeScript in this project — services, models, resources, modules, app code, scripts.
10
+
11
+ > **Sub-agent rule:** Before writing any code, read this file end-to-end and apply every rule. When spawning a sub-agent that will write code, prepend: _"First, read `skills/code-standards/SKILL.md` and apply every rule."_
12
+
13
+ The bar is **senior-level clean code**. Not "it compiles" — code that the next person on this codebase can read in one pass.
14
+
15
+ ---
16
+
17
+ ## 1. TypeScript
18
+
19
+ ### 1.1 `interface` for contracts, `type` for data
20
+
21
+ - `interface` describes a **contract** — anything with methods, anything a class implements, anything that holds actions.
22
+ - `type` describes a **data shape** — plain objects, unions, tuples, mapped types, discriminated unions.
23
+
24
+ ```typescript
25
+ // ✅ contract — has behaviour
26
+ export interface PaymentGateway {
27
+ readonly name: string;
28
+ charge(amount: number, token: string): Promise<ChargeResult>;
29
+ }
30
+
31
+ // ✅ data shape
32
+ export type ChargeResult = {
33
+ id: string;
34
+ status: "succeeded" | "failed";
35
+ amount: number;
36
+ };
37
+
38
+ // ❌ don't use interface for plain data
39
+ export interface ChargeResult {
40
+ id: string;
41
+ status: "succeeded" | "failed";
42
+ amount: number;
43
+ }
44
+ ```
45
+
46
+ ### 1.2 Access modifiers on every class member
47
+
48
+ Never omit `public` / `private` / `protected`. Explicit is faster to read than inferring.
49
+
50
+ ```typescript
51
+ // ✅
52
+ export class CartService {
53
+ public readonly currency: string;
54
+ private items: CartItem[] = [];
55
+ public add(item: CartItem) { /* ... */ }
56
+ }
57
+
58
+ // ❌
59
+ export class CartService {
60
+ readonly currency: string; // missing public
61
+ items: CartItem[] = []; // missing private
62
+ add(item: CartItem) {} // missing public
63
+ }
64
+ ```
65
+
66
+ ### 1.3 No `any`
67
+
68
+ Never. Use `unknown` when the type is genuinely unknown and narrow at the boundary. If you reach for `any`, you've skipped a design decision.
69
+
70
+ ### 1.4 No inline-duplicated unions or literals
71
+
72
+ If a union appears in more than one place, extract it to a named exported type once.
73
+
74
+ ```typescript
75
+ // ✅
76
+ export type OrderStatus = "pending" | "paid" | "shipped" | "cancelled";
77
+
78
+ export type Order = { status: OrderStatus; /* ... */ };
79
+ export type OrderEvent = { status: OrderStatus; /* ... */ };
80
+
81
+ // ❌ inline duplication — changing the set means N edits and N chances to miss one
82
+ export type Order = { status: "pending" | "paid" | "shipped" | "cancelled"; /* ... */ };
83
+ export type OrderEvent = { status: "pending" | "paid" | "shipped" | "cancelled"; /* ... */ };
84
+ ```
85
+
86
+ ### 1.5 Constrain generics
87
+
88
+ Open generics are almost always wrong. If `T` has a contract, declare it.
89
+
90
+ ```typescript
91
+ // ✅
92
+ export function publish<T extends DomainEvent>(event: T): void { /* ... */ }
93
+
94
+ // ❌
95
+ export function publish<T>(event: T): void { /* ... */ }
96
+ ```
97
+
98
+ ### 1.6 Prefer `undefined` for "absent"
99
+
100
+ Use `undefined` everywhere — optional properties, optional parameters, "not found" returns. Optional chaining (`?.`) and default values (`??`) compose naturally with `undefined`. Use `null` **only** when an external API hands you `null` (e.g. a database driver, a third-party SDK) and convert at the boundary.
101
+
102
+ ```typescript
103
+ // ✅
104
+ export type User = { id: string; phone?: string };
105
+ function find(id: string): User | undefined { /* ... */ }
106
+
107
+ // ❌ mixed
108
+ export type User = { id: string; phone: string | null };
109
+ ```
110
+
111
+ ---
112
+
113
+ ## 2. Imports
114
+
115
+ ### 2.1 Use the project's path aliases, not deep relatives
116
+
117
+ The project ships a `tsconfig.json` path alias for the app root — `app/*` → `src/app/*`. Use it for every cross-module import. If you see `../../../../something` in an import, you're bypassing it.
118
+
119
+ ```typescript
120
+ // ✅
121
+ import { listUsersService } from "app/users/services";
122
+ import { OrderStatus } from "app/orders/types/order.type";
123
+
124
+ // ❌
125
+ import { listUsersService } from "../../users/services";
126
+ ```
127
+
128
+ ### 2.2 Barrel files — sub-folder level, not module level
129
+
130
+ Barrels (`index.ts`) make a module easier to consume **at the sub-folder level** — `services/`, `utils/`, `types/`. They become a problem when you put one at the **module root** and turn it into a giant re-export of everything inside.
131
+
132
+ The right shape:
133
+
134
+ ```
135
+ ✅ app/users/services/index.ts ← barrels each service file
136
+ app/users/services/list-users.service.ts
137
+ app/users/services/create-user.service.ts
138
+ app/users/utils/index.ts ← barrels each util
139
+ app/users/types/index.ts ← barrels each type
140
+
141
+ ❌ app/users/index.ts ← module-root barrel re-exporting everything
142
+ ```
143
+
144
+ Imports stay short and intent-revealing:
145
+
146
+ ```typescript
147
+ // ✅ targeted — reader knows it's a service
148
+ import { listUsersService } from "app/users/services";
149
+ import { formatUserName } from "app/users/utils";
150
+
151
+ // ❌ everything-from-everywhere
152
+ import { listUsersService, formatUserName, User } from "app/users";
153
+ ```
154
+
155
+ The module-root barrel is the one that causes circular imports and tree-shaking misses. Sub-folder barrels are safe because they only group siblings of the same role.
156
+
157
+ ### 2.3 No file extension in import paths
158
+
159
+ Always bare: `from "./order.service"`, never `from "./order.service.ts"` or `.js`.
160
+
161
+ ---
162
+
163
+ ## 3. File naming
164
+
165
+ | Suffix | Purpose | Example |
166
+ | -------------- | ------------------------------------ | ---------------------- |
167
+ | `.contract.ts` | Interface only (blueprint) | `payment.contract.ts` |
168
+ | `.type.ts` | Types only (data shapes) | `order.type.ts` |
169
+ | `.service.ts` | Service class | `cart.service.ts` |
170
+ | `.model.ts` | Domain model class | `user.model.ts` |
171
+ | `.resource.ts` | Output mapper (model → wire format) | `order.resource.ts` |
172
+ | `.spec.ts` | Co-located test for a sibling `.ts` | `cart.service.spec.ts` |
173
+
174
+ **One primary export per file.** If a `.contract.ts` also defines closely-related data types used only by that contract, that's fine — but the primary export is the interface.
175
+
176
+ ---
177
+
178
+ ## 4. Code organization
179
+
180
+ ### 4.1 One responsibility per file, class, and function
181
+
182
+ If the file's description needs an "and", split it. The same goes for classes and functions. A 300-line service that handles orders **and** sends emails **and** writes audit logs is three files.
183
+
184
+ **File-level SRP.** If a file has more than one top-level helper that isn't a direct implementation detail of the main export, extract helpers into a `utils/` folder — one helper per file. The main file imports from leaf paths.
185
+
186
+ ```
187
+ ❌ cart.service.ts holds CartService + calculateTax + formatLineItem + applyDiscount
188
+ ✅ cart.service.ts holds CartService only
189
+ utils/calculate-tax.ts
190
+ utils/format-line-item.ts
191
+ utils/apply-discount.ts
192
+ ```
193
+
194
+ ### 4.2 Public API is functional, classes are an implementation detail
195
+
196
+ The default shape of a public primitive is a **factory function returning a plain object**. Users of your service never call `new`.
197
+
198
+ Reach for a `class` in exactly three cases:
199
+
200
+ 1. **Long-lived state across calls.** A client, a cache, a connection pool. Example: a `PaymentClient` that owns an HTTP connection and an auth token for its lifetime.
201
+ 2. **Per-call state across phases.** A single operation has 4+ phases that share mutable state (intermediate results, accumulated errors, events). Instantiate fresh per call inside the factory; keep the class unexported.
202
+ 3. **Immutable builder pattern.** A value type whose methods each return a new snapshot of itself. The class gives you prototype method sharing and `instanceof` narrowing. Still front it with a factory so callers never see `new`.
203
+
204
+ If state fits in local variables of a single function, **don't make a class** — a closure is cleaner.
205
+
206
+ ```typescript
207
+ // ✅ public factory, internal class for per-call phases
208
+ export function placeOrder(config: OrderConfig) {
209
+ return {
210
+ async execute(input: OrderInput) {
211
+ return new PlacementRun(config, input).run();
212
+ },
213
+ };
214
+ }
215
+
216
+ class PlacementRun {
217
+ private items: ResolvedItem[] = [];
218
+ private total = 0;
219
+ public async run(): Promise<OrderResult> { /* ... */ }
220
+ private async validate(): Promise<void> { /* ... */ }
221
+ private async charge(): Promise<void> { /* ... */ }
222
+ }
223
+ ```
224
+
225
+ ### 4.3 No single-file folders
226
+
227
+ Never create a folder to hold one file and an `index.ts` that re-exports it. Put the file in the parent.
228
+
229
+ ```
230
+ ❌ src/services/cart/cart.service.ts
231
+ src/services/cart/index.ts ← only re-exports
232
+ ✅ src/services/cart.service.ts
233
+ ```
234
+
235
+ A folder earns its keep at **two or more related files**.
236
+
237
+ ### 4.4 Readability beats terseness
238
+
239
+ Code is read far more than it's written. Optimize for the reader.
240
+
241
+ - **Always brace.** Never single-line `if (x) doIt();`, never braceless bodies, never inline for-loop bodies. The one exception: a guard clause that ends the function — `if (!user) return;` is fine on a single line.
242
+ - **Full, descriptive identifiers.** No cryptic abbreviations. Write `systemPrompt`, `tripIndex`, `request`, `response`, `error`, `message`, `tool`. Not `sp`, `i`, `req`, `res`, `err`, `msg`, `t`. This includes arrow-function parameters — `.find((tool) => ...)`, never `.find((t) => ...)`. Local scope isn't a license to abbreviate — the reader has the **least** context locally.
243
+ - **One statement per line.** No chaining three actions onto a single `if`. No packing two branches into a ternary when `if/else` reads clearer.
244
+ - **Avoid deeply nested ternaries.** If a ternary wraps or has three branches, rewrite as `if/else`.
245
+ - **One class per file.** Always.
246
+
247
+ ### 4.5 Blank lines separate consecutive logical blocks
248
+
249
+ Consecutive control structures (`if`, `for`, `while`, `try`, `switch`) **are not a wall** — each one is its own logical step. Put a blank line **between** them.
250
+
251
+ ```typescript
252
+ // ❌ wall — hard to scan
253
+ if (!user) {
254
+ return notFound();
255
+ }
256
+ if (!user.verified) {
257
+ return forbidden();
258
+ }
259
+ for (const item of items) {
260
+ await process(item);
261
+ }
262
+
263
+ // ✅ each block breathes
264
+ if (!user) {
265
+ return notFound();
266
+ }
267
+
268
+ if (!user.verified) {
269
+ return forbidden();
270
+ }
271
+
272
+ for (const item of items) {
273
+ await process(item);
274
+ }
275
+ ```
276
+
277
+ Same rule inside a function — put a blank line before a `return` and between phases that "finish a thought" (e.g. a `.push()` that ends an accumulation step before the next phase begins). Don't over-apply it inside a single block — code inside one `if` body usually doesn't need internal blank lines.
278
+
279
+ ### 4.6 Comments — default to none
280
+
281
+ Identifiers carry the meaning. Add a comment only when the **why** is non-obvious: a hidden constraint, a subtle invariant, a workaround for a bug elsewhere. **Never** describe what the code does — if the reader can't see it from the code, the code is the problem.
282
+
283
+ ```typescript
284
+ // ❌ noise
285
+ // Loop over items
286
+ for (const item of items) { /* ... */ }
287
+
288
+ // ✅ the WHY isn't obvious from the code
289
+ // Stripe returns idempotency_key on retry but not on first call —
290
+ // fall back to the order id so downstream dedup still works.
291
+ const key = response.idempotency_key ?? order.id;
292
+ ```
293
+
294
+ ### 4.7 Pure helpers live outside classes
295
+
296
+ If a function doesn't touch the class's state and isn't part of the class's interface, it's not a method. Move it to a sibling utility file. Methods are for behaviour that operates on the instance.
297
+
298
+ ---
299
+
300
+ ## 5. Errors
301
+
302
+ ### 5.1 Extend framework error classes
303
+
304
+ Never throw bare `Error`. Every project error extends a framework error so the dispatcher, logger, and HTTP layer can classify it correctly. Match the closest base — `HttpError` for request-failure cases, a domain root for business-rule violations.
305
+
306
+ Define the class once, then throw it from the call site by name — the type itself carries the meaning.
307
+
308
+ ```typescript
309
+ // ✅ define
310
+ import { HttpError } from "@warlock.js/core";
311
+
312
+ export class CartLockedError extends HttpError {
313
+ public readonly statusCode = 409;
314
+ public constructor(public readonly cartId: string) {
315
+ super(`Cart ${cartId} is locked`);
316
+ }
317
+ }
318
+
319
+ // ✅ throw — the class name is the error code, the message stays useful
320
+ import { CartLockedError } from "app/cart/errors/cart-locked.error";
321
+
322
+ if (cart.isLocked) {
323
+ throw new CartLockedError(cart.id);
324
+ }
325
+
326
+ // ❌ bare Error — no status code, no type to catch, no classification
327
+ throw new Error("cart locked");
328
+ ```
329
+
330
+ ### 5.2 Handle at boundaries, not in the middle
331
+
332
+ Wrap a `try / catch` only at system boundaries — an HTTP handler, a queue consumer, a CLI entry point — or when you're **enriching** the error with context before re-throwing. Internal function calls should let errors propagate; the boundary catches once and decides the response.
333
+
334
+ ### 5.3 Never swallow
335
+
336
+ A bare `catch {}` or `catch (e) { console.log(e); }` is a bug, not error handling. If you can't recover, re-throw. If you can recover, log the cause and continue with an explicit fallback value.
337
+
338
+ ---
339
+
340
+ ## 6. Async
341
+
342
+ ### 6.1 Parallelize independent awaits
343
+
344
+ Two independent async calls in a row is two round-trips. Use `Promise.all`.
345
+
346
+ ```typescript
347
+ // ❌ serial — twice the latency for no reason
348
+ const user = await loadUser(id);
349
+ const orders = await loadOrders(id);
350
+
351
+ // ✅ parallel
352
+ const [user, orders] = await Promise.all([loadUser(id), loadOrders(id)]);
353
+ ```
354
+
355
+ ### 6.2 Pass `AbortSignal` where supported
356
+
357
+ If a framework call (HTTP, AI, queue) accepts a signal, plumb one through. Long-running operations that ignore cancellation leak resources on user disconnect.
358
+
359
+ ### 6.3 No floating promises
360
+
361
+ Every promise is either awaited, returned, or explicitly `.catch()`'d. A "fire and forget" call that throws will crash the process or vanish silently — both bad.
362
+
363
+ ```typescript
364
+ // ❌
365
+ sendWelcomeEmail(user); // floating — if it rejects, you'll never know
366
+
367
+ // ✅
368
+ await sendWelcomeEmail(user);
369
+
370
+ // ✅ deliberate fire-and-forget with logged failure
371
+ sendWelcomeEmail(user).catch((error) => logger.error("welcome-email-failed", error));
372
+ ```
373
+
374
+ ---
375
+
376
+ ## 7. Documentation (JSDoc)
377
+
378
+ Project-level JSDoc is **lighter** than framework-package JSDoc — the audience already knows the codebase. JSDoc earns its place when it tells the reader something the code can't.
379
+
380
+ ### 7.1 Required
381
+
382
+ - **Every exported function, class, and type that crosses a module boundary.** One-line purpose, plus `@example` if the call shape isn't obvious.
383
+ - **Every class with more than one public method.** A class-level block stating the role and what it owns / doesn't own. Don't make the next reader infer the architecture from method bodies.
384
+ - **Any function whose logic isn't obvious from its name.** Regex, state machines, multi-branch business rules, performance-driven code, anything with a non-obvious constraint.
385
+
386
+ ### 7.2 Not required
387
+
388
+ - Private and protected methods, unless the **why** is non-obvious.
389
+ - Trivial getters, setters, and one-line helpers.
390
+ - Internal functions whose name and signature already say everything.
391
+
392
+ ### 7.3 Purpose, not restatement
393
+
394
+ The first line states the **role** — what this thing is for, not what its name already says.
395
+
396
+ ```typescript
397
+ // ❌ adds nothing
398
+ /** Builds the message list. */
399
+ private buildMessages(): void { /* ... */ }
400
+
401
+ // ✅ explains the role and the position in the flow
402
+ /**
403
+ * Resolve the system prompt, prepend history, append the user input.
404
+ * Produces the initial messages array sent on the first trip. Runs once
405
+ * per execution before the trip loop starts.
406
+ */
407
+ private buildMessages(): void { /* ... */ }
408
+ ```
409
+
410
+ ---
411
+
412
+ ## 8. Logging
413
+
414
+ Use the `log` singleton from `@warlock.js/logger`. Never `console.log` in committed code.
415
+
416
+ Every call has the same shape — **`module`**, **`action`**, **`message`**, optional **`context`**:
417
+
418
+ ```typescript
419
+ import { log } from "@warlock.js/logger";
420
+
421
+ // ✅ four arguments — module, action, message, context
422
+ log.success("orders", "order-placed", "Order has been created successfully", {
423
+ orderId,
424
+ userId,
425
+ total,
426
+ });
427
+
428
+ log.error("payments", "charge", new Error("Card declined"), { userId, attemptId });
429
+
430
+ // ❌ interpolated string — unstructured, can't be queried
431
+ log.info(`Order ${orderId} placed for user ${userId} totaling ${total}`);
432
+
433
+ // ❌ wrong arity — missing module/action
434
+ log.info("order-placed", { orderId, userId, total });
435
+ ```
436
+
437
+ **Five levels** — `debug`, `info`, `warn`, `error`, `success`. Pick the one that matches the event semantics. `success` is its own level (not a flavour of `info`) — use it for explicit completions a human cares about.
438
+
439
+ **Other rules:**
440
+
441
+ - Log at boundaries (request in, request out, error caught) — not every internal step.
442
+ - **Redact at the source.** Tokens, passwords, credit card numbers, personal identifiers never appear in log context. Configure redaction once via `log.configure({ redact: ... })` — see `@warlock.js/logger/redact-sensitive-log-fields/SKILL.md`.
443
+ - `module` is the area of the app (`"orders"`, `"auth"`, `"payments"`), `action` is the verb-noun event (`"order-placed"`, `"login-failed"`, `"refund-issued"`). Both are searchable indexes — keep them consistent across the codebase.
444
+
445
+ ---
446
+
447
+ ## 9. Non-negotiable defaults
448
+
449
+ Senior-team baseline rules. Each one prevents a real class of bug or incident. Each one is short because each one is unconditional. Deeper coverage lives in sibling skills — `skills/security-baseline/`, `skills/data-and-persistence/`, `skills/api-design/`, `skills/observability-and-resilience/`.
450
+
451
+ ### 9.1 Validate untrusted input at the boundary
452
+
453
+ Every input from outside the process — request body, query params, headers, file uploads, env vars, queue messages, webhook payloads — is validated against a schema before any code consumes it. TypeScript types are runtime lies; only a validator gives you actual guarantees.
454
+
455
+ Use `@warlock.js/seal`. Schemas live in the module's `schema/` folder, exported alongside the inferred type.
456
+
457
+ ```typescript
458
+ // ✅ schema + inferred type — colocated, single source
459
+ import { v, type Infer } from "@warlock.js/seal";
460
+
461
+ export const createOrderSchema = v.object({
462
+ cartId: v.string(),
463
+ paymentToken: v.string(),
464
+ });
465
+
466
+ export type CreateOrderSchema = Infer<typeof createOrderSchema>;
467
+
468
+ // ❌ trust the type — no runtime check, any payload passes
469
+ const input = request.body as CreateOrderSchema;
470
+ ```
471
+
472
+ Validation runs once at the route boundary; everything inward consumes the typed result. See `skills/security-baseline/SKILL.md` for the full input-trust model.
473
+
474
+ ### 9.2 Money is integer minor units, time is UTC at rest
475
+
476
+ - **Money:** integer cents (or the currency's smallest unit). Never a `number` field for prices, totals, balances, refunds. Floats break the first time you sum them.
477
+ - **Time:** store UTC in the database, work in UTC across services, convert to the user's timezone **only** at the response layer.
478
+
479
+ ```typescript
480
+ // ✅
481
+ type Order = { totalCents: number; placedAtUtc: Date };
482
+
483
+ // ❌
484
+ type Order = { total: number; placedAt: string }; // float money, ambiguous tz
485
+ ```
486
+
487
+ See `skills/data-and-persistence/SKILL.md`.
488
+
489
+ ### 9.3 IDs in URLs are opaque
490
+
491
+ Use UUIDs or nanoids for any identifier that crosses a process boundary — URLs, response bodies, log lines, webhooks. Never expose sequential primary keys: they leak business volume (anyone can tell `/orders/1042` means you've placed 1042 orders) and invite enumeration attacks.
492
+
493
+ Sequential keys are fine as internal database primary keys. They just don't leave the database.
494
+
495
+ ### 9.4 Every external call has a timeout
496
+
497
+ HTTP, AI, queue, database driver — every call to something outside this process has an explicit timeout. No timeout = a slow dependency takes down the whole service.
498
+
499
+ `@warlock.js/http` accepts `timeout` per request and `timeout` per `Http` instance. Set a default at the instance level; override per request when a specific endpoint needs more.
500
+
501
+ See `skills/observability-and-resilience/SKILL.md`.
502
+
503
+ ### 9.5 Idempotency keys for external side effects
504
+
505
+ Payments, emails, webhook deliveries, third-party charges — any operation whose accidental retry could double-charge a customer or send a duplicate notice needs an idempotency key. Generate the key at the call site, store the result keyed by the idempotency key, return the stored result on retry.
506
+
507
+ This is the difference between "the network blipped" and "the customer got billed twice".
508
+
509
+ ### 9.6 One response envelope across the project
510
+
511
+ Every HTTP response in the project uses the same shape — success and failure both. Pick once, document in `skills/api-design/SKILL.md`, use everywhere. The moment this rule lands, the frontend stops writing per-endpoint special cases.
512
+
513
+ ### 9.7 Config validated at boot, no scattered `process.env`
514
+
515
+ Every environment variable is read through `env()` inside a `src/config/*.ts` file — one typed config object per concern (`app`, `database`, `mail`, …), each with a sensible default. App code never touches `process.env.X`; it reads resolved values through the `config` accessor from `@warlock.js/core`.
516
+
517
+ ```typescript
518
+ // ✅ src/config/app.ts — env() with defaults, typed config object
519
+ import { env, type AppConfigurations } from "@warlock.js/core";
520
+
521
+ const appConfig: AppConfigurations = {
522
+ appName: env("APP_NAME", "Warlock"),
523
+ baseUrl: env("BASE_URL", "http://localhost:2030"),
524
+ };
525
+
526
+ export default appConfig;
527
+
528
+ // ✅ read a resolved value anywhere via the config accessor
529
+ import { config } from "@warlock.js/core";
530
+
531
+ const appName = config.key<string>("app.appName");
532
+ const database = config.get("database"); // whole typed group
533
+
534
+ // ❌ scattered, unvalidated process.env reads in app code
535
+ const url = process.env.BASE_URL;
536
+ ```
537
+
538
+ A secret with no safe default (a payment key, a signing secret) should fail loudly at boot — give the config file an explicit guard rather than letting a missing value surface three hours in on the first request.
539
+
540
+ ---
541
+
542
+ ## 10. Testing
543
+
544
+ - **Vitest**, co-located: `cart.service.ts` → `cart.service.spec.ts` in the same folder.
545
+ - **Test the public surface.** If a private method needs testing, the public method that calls it does — test through it. If you can't reach the behaviour from outside, it shouldn't exist.
546
+ - **Arrange / Act / Assert** structure per test. Blank lines between phases. One assertion target per test where practical.
547
+ - **Mock at the boundary.** Use `MockSDK` from `@warlock.js/ai` for AI calls. Mock HTTP at the HTTP client level, not deep inside services.
548
+ - **Don't snapshot large objects.** Assert specific fields. A snapshot diff with 200 lines of noise hides the regression you actually care about.
549
+
550
+ ---
551
+
552
+ ## 11. Tooling
553
+
554
+ Ship a `.eslintrc` with at least these rules on:
555
+
556
+ - `@typescript-eslint/explicit-member-accessibility` — enforces § 1.2
557
+ - `@typescript-eslint/no-explicit-any` — enforces § 1.3
558
+ - `@typescript-eslint/consistent-type-definitions` — supports § 1.1
559
+ - `@typescript-eslint/no-floating-promises` — enforces § 6.3
560
+ - `@typescript-eslint/await-thenable`
561
+ - `curly` — enforces braces (§ 4.4)
562
+ - `no-console` — enforces § 8
563
+
564
+ Match the project's `.prettierrc` exactly. If two configs are present and disagree, the one closer to the file wins (Prettier's resolution order). Don't fight the formatter — configure it, then trust it.
565
+
566
+ ---
567
+
568
+ ## 12. Review checklist
569
+
570
+ Before any change ships:
571
+
572
+ **Craft**
573
+ - [ ] All class members have access modifiers
574
+ - [ ] No `any`
575
+ - [ ] No inline-duplicated unions or literal sets
576
+ - [ ] `interface` for contracts, `type` for data
577
+ - [ ] File suffix matches content
578
+ - [ ] Imports use project path aliases, not deep relatives
579
+ - [ ] No module-root barrels, no single-file folders
580
+ - [ ] Blank lines separate consecutive control blocks
581
+ - [ ] No floating promises, no swallowed catches
582
+ - [ ] Errors extend framework error classes
583
+ - [ ] JSDoc on every public export and every multi-method class
584
+ - [ ] No `console.log`, `log.<level>(module, action, message, context)` signature used correctly
585
+ - [ ] Tests co-located, public-surface-driven
586
+ - [ ] `tsc --noEmit` and `eslint` pass clean
587
+
588
+ **Non-negotiable defaults**
589
+ - [ ] Every untrusted input has a `@warlock.js/seal` schema at the boundary
590
+ - [ ] Money fields are integer minor units, time stored as UTC
591
+ - [ ] IDs exposed in URLs are opaque (UUID / nanoid)
592
+ - [ ] Every external call (HTTP, AI, queue, DB) has an explicit timeout
593
+ - [ ] External side effects (payments, emails, webhooks) use idempotency keys
594
+ - [ ] Response shape matches the project's canonical envelope
595
+ - [ ] No `process.env.X` outside the config module