create-warlock 5.34.1 → 5.34.3

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 (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/llms-full.txt +21 -28
  3. package/llms.txt +1 -1
  4. package/package.json +2 -2
  5. package/skills/{create-a-warlock-project/SKILL.md → SKILL.md} +20 -27
  6. package/templates/toolchains/oxc/.oxlintrc.json +3 -1
  7. package/templates/warlock/.oxlintrc.json +3 -1
  8. package/templates/warlock/AGENTS.md +10 -10
  9. package/templates/warlock/skills/api-design/SKILL.md +110 -50
  10. package/templates/warlock/skills/code-standards/SKILL.md +48 -39
  11. package/templates/warlock/skills/data-and-persistence/SKILL.md +72 -57
  12. package/templates/warlock/skills/git-workflow/SKILL.md +9 -9
  13. package/templates/warlock/skills/module-boundaries/SKILL.md +2 -2
  14. package/templates/warlock/skills/observability-and-resilience/SKILL.md +8 -6
  15. package/templates/warlock/skills/security-baseline/SKILL.md +23 -22
  16. package/templates/warlock/skills/testing-strategy/SKILL.md +7 -7
  17. package/templates/warlock/skills/web-code-standards/SKILL.md +157 -60
  18. package/templates/warlock/src/app/auth/models/otp/otp.model.ts +9 -3
  19. package/templates/warlock/src/app/auth/services/otp.service.ts +18 -6
  20. package/templates/warlock/src/app/contact/controllers/contact.controller.ts +6 -0
  21. package/templates/warlock/src/app/locale/controllers/locale.controller.ts +6 -0
  22. package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +7 -0
  23. package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +14 -9
  24. package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +1 -0
  25. package/templates/warlock/src/app/posts/models/post/post.model.ts +6 -0
  26. package/templates/warlock/src/app/posts/resources/post.resource.ts +1 -1
  27. package/templates/warlock/src/app/posts/schema/update-post.schema.ts +1 -0
  28. package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +41 -0
  29. package/templates/warlock/src/app/users/commands/hello-world.command.ts +2 -1
  30. package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +12 -1
  31. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +24 -1
  32. package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
  33. package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
  34. package/templates/warlock/src/app/users/repositories/users.repository.ts +2 -2
  35. package/templates/warlock/src/app/users/resources/user.resource.ts +1 -1
  36. package/templates/warlock/src/app/users/schema/list-users.schema.ts +1 -1
  37. package/templates/warlock/src/config/http.ts +6 -0
  38. package/templates/warlock/src/web/home/components/contact-field.tsx +18 -0
  39. package/templates/warlock/src/web/home/components/contact-section.tsx +5 -29
  40. package/templates/warlock/src/web/home/components/contact-submit-button.tsx +16 -0
  41. package/templates/warlock/src/web/home/hooks/use-contact-field.ts +9 -0
  42. package/templates/warlock/src/web/home/hooks/use-contact-form.ts +5 -8
  43. package/templates/warlock/src/web/home/hooks/use-contact-locale.ts +34 -0
  44. package/templates/warlock/src/web/home/hooks/use-contact-submit-button.ts +6 -0
  45. package/templates/warlock/src/web/home/index.page.tsx +1 -1
  46. package/templates/warlock/src/web/home/types/contact-controls.type.ts +13 -0
  47. package/templates/warlock/src/web/shared/utils/set-form-errors.ts +3 -1
  48. package/templates/warlock/src/web/home/components/contact-form-controls.tsx +0 -42
@@ -1,6 +1,6 @@
1
1
  ---
2
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, Oxlint/Oxfmt. 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.'
3
+ description: "Shared TypeScript rules: contracts versus data types, function services, file names, app barrels, errors, async work, logging and Oxc enforcement. Use when writing or reviewing TypeScript. Use web-code-standards instead for web components, hooks and setup boundaries."
4
4
  ---
5
5
 
6
6
  # Code Standards
@@ -8,7 +8,7 @@ description: 'Project-level TypeScript code standards for apps built on Warlock.
8
8
  **Status:** Stable
9
9
  **Applies to:** All TypeScript in this project — services, models, resources, modules, app code, scripts.
10
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."_
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 the `code-standards` skill and apply every rule."_
12
12
 
13
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
14
 
@@ -18,6 +18,7 @@ The bar is **senior-level clean code**. Not "it compiles" — code that the next
18
18
 
19
19
  ### 1.1 `interface` for contracts, `type` for data
20
20
 
21
+ - Declaration-merging targets (for example framework module augmentations) also require `interface`.
21
22
  - `interface` describes a **contract** — anything with methods, anything a class implements, anything that holds actions.
22
23
  - `type` describes a **data shape** — plain objects, unions, tuples, mapped types, discriminated unions.
23
24
 
@@ -49,7 +50,7 @@ Never omit `public` / `private` / `protected`. Explicit is faster to read than i
49
50
 
50
51
  ```typescript
51
52
  // ✅
52
- export class CartService {
53
+ export class Cart {
53
54
  public readonly currency: string;
54
55
  private items: CartItem[] = [];
55
56
  public add(item: CartItem) {
@@ -58,7 +59,7 @@ export class CartService {
58
59
  }
59
60
 
60
61
  // ❌
61
- export class CartService {
62
+ export class Cart {
62
63
  readonly currency: string; // missing public
63
64
  items: CartItem[] = []; // missing private
64
65
  add(item: CartItem) {} // missing public
@@ -160,7 +161,7 @@ import { formatUserName } from "app/users/utils";
160
161
  import { listUsersService, formatUserName, User } from "app/users";
161
162
  ```
162
163
 
163
- 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.
164
+ The module-root barrel is the one that causes circular imports and tree-shaking misses. This recommendation applies to server app modules under `src/app/**`. Web feature folders under `src/web/**` have no barrels: imports point to the exact component or hook file so client bundles stay lean and server-only code is never pulled through a barrel. See the `web-code-standards` skill.
164
165
 
165
166
  ### 2.3 No file extension in import paths
166
167
 
@@ -170,14 +171,24 @@ Always bare: `from "./order.service"`, never `from "./order.service.ts"` or `.js
170
171
 
171
172
  ## 3. File naming
172
173
 
173
- | Suffix | Purpose | Example |
174
- | -------------- | ----------------------------------- | ---------------------- |
175
- | `.contract.ts` | Interface only (blueprint) | `payment.contract.ts` |
176
- | `.type.ts` | Types only (data shapes) | `order.type.ts` |
177
- | `.service.ts` | Service class | `cart.service.ts` |
178
- | `.model.ts` | Domain model class | `user.model.ts` |
179
- | `.resource.ts` | Output mapper (model → wire format) | `order.resource.ts` |
180
- | `.spec.ts` | Co-located test for a sibling `.ts` | `cart.service.spec.ts` |
174
+ | Suffix | Purpose | Example |
175
+ | ------------------------- | ---------------------------------------------- | --------------------------------------- |
176
+ | `.contract.ts` | Interface contract | `payment.contract.ts` |
177
+ | `.type.ts` | Data types | `order.type.ts` |
178
+ | `.service.ts` | Named service function | `list-users.service.ts` |
179
+ | `.controller.ts` | HTTP handler function | `me.controller.ts` |
180
+ | `.repository.ts` | Framework repository subclass and instance | `users.repository.ts` |
181
+ | `.schema.ts` | Seal schema and inferred type | `create-user.schema.ts` |
182
+ | `.error.ts` | Framework error subclass | `order-conflict.error.ts` |
183
+ | `.middleware.ts` | Request middleware function | `require-owner.middleware.ts` |
184
+ | `.model.ts` | Cascade model subclass | `user.model.ts` |
185
+ | `.migration.ts` | Timestamped model migration | `20-10-2026_12-00-00-user.migration.ts` |
186
+ | `.seeder.ts` / `.seed.ts` | Seeder (starter uses `.seed.ts`) | `users.seed.ts` |
187
+ | `.page.tsx` | Web page component | `index.page.tsx` |
188
+ | `.setup.ts` | Page, layout or root policy and server exports | `index.setup.ts` |
189
+ | `use-*.ts` | Named feature hook | `use-contact-form.ts` |
190
+ | `.resource.ts` | Output mapper | `user.resource.ts` |
191
+ | `.spec.ts` / `.test.ts` | Co-located test | `cart.service.spec.ts` |
181
192
 
182
193
  **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.
183
194
 
@@ -192,8 +203,8 @@ If the file's description needs an "and", split it. The same goes for classes an
192
203
  **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.
193
204
 
194
205
  ```
195
- ❌ cart.service.ts holds CartService + calculateTax + formatLineItem + applyDiscount
196
- ✅ cart.service.ts holds CartService only
206
+ ❌ cart.service.ts holds cartService + calculateTax + formatLineItem + applyDiscount
207
+ ✅ cart.service.ts holds cartService only
197
208
  utils/calculate-tax.ts
198
209
  utils/format-line-item.ts
199
210
  utils/apply-discount.ts
@@ -201,7 +212,7 @@ If the file's description needs an "and", split it. The same goes for classes an
201
212
 
202
213
  ### 4.2 Public API is functional, classes are an implementation detail
203
214
 
204
- The default shape of a public primitive is a **factory function returning a plain object**. Users of your service never call `new`.
215
+ This rule governs app code: services are named functions such as `listUsersService(input)`. A stateful app primitive may use a **factory function returning a plain object**. Framework base classes are extended as their package skills require: models, repositories, connectors, and errors remain subclasses; resources returned by `defineResource` are constructed with `new`.
205
216
 
206
217
  Reach for a `class` in exactly three cases:
207
218
 
@@ -304,7 +315,7 @@ for (const item of items) {
304
315
  // ✅ the WHY isn't obvious from the code
305
316
  // Stripe returns idempotency_key on retry but not on first call —
306
317
  // fall back to the order id so downstream dedup still works.
307
- const key = response.idempotency_key ?? order.id;
318
+ const key = response.idempotencyKey ?? order.id;
308
319
  ```
309
320
 
310
321
  ### 4.7 Pure helpers live outside classes
@@ -323,20 +334,19 @@ Define the class once, then throw it from the call site by name — the type its
323
334
 
324
335
  ```typescript
325
336
  // ✅ define
326
- import { HttpError } from "@warlock.js/core";
337
+ import { ConflictError } from "@warlock.js/core";
327
338
 
328
- export class CartLockedError extends HttpError {
329
- public readonly statusCode = 409;
339
+ export class CartLockedError extends ConflictError {
330
340
  public constructor(public readonly cartId: string) {
331
341
  super(`Cart ${cartId} is locked`);
332
342
  }
333
343
  }
334
344
 
335
- // ✅ throw — the class name is the error code, the message stays useful
345
+ // ✅ throw — the class identifies the exception; the HTTP formatter supplies the wire code
336
346
  import { CartLockedError } from "app/cart/errors/cart-locked.error";
337
347
 
338
348
  if (cart.isLocked) {
339
- throw new CartLockedError(cart.id);
349
+ throw new CartLockedError(cart.get<string>("publicId"));
340
350
  }
341
351
 
342
352
  // ❌ bare Error — no status code, no type to catch, no classification
@@ -455,14 +465,14 @@ log.info("order-placed", { orderId, userId, total });
455
465
  **Other rules:**
456
466
 
457
467
  - Log at boundaries (request in, request out, error caught) — not every internal step.
458
- - **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`.
468
+ - **Redact at the source.** Tokens, passwords, credit card numbers, personal identifiers never appear in log context. Configure redaction once via `log.configure({ redact: ... })` — see the `@warlock.js/logger` skill's `redact-sensitive-log-fields` topic.
459
469
  - `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.
460
470
 
461
471
  ---
462
472
 
463
473
  ## 9. Non-negotiable defaults
464
474
 
465
- 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/`.
475
+ 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 — the `security-baseline` skill, the `data-and-persistence` skill, the `api-design` skill, the `observability-and-resilience` skill.
466
476
 
467
477
  ### 9.1 Validate untrusted input at the boundary
468
478
 
@@ -485,7 +495,7 @@ export type CreateOrderSchema = Infer<typeof createOrderSchema>;
485
495
  const input = request.body as CreateOrderSchema;
486
496
  ```
487
497
 
488
- 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.
498
+ Validation runs once at the route boundary; everything inward consumes the typed result. See the `security-baseline` skill for the full input-trust model.
489
499
 
490
500
  ### 9.2 Money is integer minor units, time is UTC at rest
491
501
 
@@ -500,13 +510,13 @@ type Order = { totalCents: number; placedAtUtc: Date };
500
510
  type Order = { total: number; placedAt: string }; // float money, ambiguous tz
501
511
  ```
502
512
 
503
- See `skills/data-and-persistence/SKILL.md`.
513
+ See the `data-and-persistence` skill.
504
514
 
505
515
  ### 9.3 IDs in URLs are opaque
506
516
 
507
- 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.
517
+ Use an opaque UUID for public resource IDs in URLs, responses, and webhooks. The starter models store a separate `publicId`, generated by `randomUUID()` from `node:crypto` through a Seal default, with a unique migration column. The resource emits `id: ["publicId", "string"]`; services look up `{ publicId: id }`, then use the model's internal identity for writes.
508
518
 
509
- Sequential keys are fine as internal database primary keys. They just don't leave the database.
519
+ Cascade's default `id` is sequential (including its NoSQL-generated integer); `model.uuid` only stringifies the primary key. Neither creates an opaque identifier. Internal primary/foreign keys may remain numeric. Never use `find(publicId)` or repository `getCached(publicId)` unless the primary key was explicitly changed. The `data-and-persistence` skill covers the schema and migration.
510
520
 
511
521
  ### 9.4 Every external call has a timeout
512
522
 
@@ -514,7 +524,7 @@ HTTP, AI, queue, database driver — every call to something outside this proces
514
524
 
515
525
  `@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.
516
526
 
517
- See `skills/observability-and-resilience/SKILL.md`.
527
+ See the `observability-and-resilience` skill.
518
528
 
519
529
  ### 9.5 Idempotency keys for external side effects
520
530
 
@@ -524,7 +534,7 @@ This is the difference between "the network blipped" and "the customer got bille
524
534
 
525
535
  ### 9.6 One response envelope across the project
526
536
 
527
- 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.
537
+ Every HTTP response follows the named success envelopes or the canonical `{ error: { code, message }, details?, requestId }` error envelope in the `api-design` skill. The moment this rule lands, the frontend stops writing per-endpoint special cases.
528
538
 
529
539
  ### 9.7 Config validated at boot, no scattered `process.env`
530
540
 
@@ -579,15 +589,14 @@ bounded regexes on input, one controller per file, `type` (not `kind`) as a
579
589
  union's discriminator. Each problem has a `WL` code and its fix
580
590
  (`npx warlock explain <CODE>`).
581
591
 
582
- For Oxlint, consider enabling these additional rules:
592
+ The scaffold enables these Oxlint rules:
583
593
 
584
- - `typescript/explicit-member-accessibility` — enforces § 1.2
585
- - `typescript/no-explicit-any` — enforces § 1.3
586
- - `typescript/consistent-type-definitions` — supports § 1.1
587
- - `typescript/no-floating-promises` — enforces § 6.3 (requires `oxlint-tsgolint` and `options.typeAware: true`)
588
- - `typescript/await-thenable` (same type-aware engine requirement)
589
- - `curly` — enforces braces (§ 4.4)
590
- - `no-console` — enforces § 8
594
+ - `typescript/explicit-member-accessibility` — § 1.2
595
+ - `typescript/no-explicit-any` — § 1.3
596
+ - `curly` — § 4.4
597
+ - `no-console` — § 8
598
+
599
+ `typescript/consistent-type-definitions` cannot express § 1.1's hybrid rule (interfaces for contracts, types for data); its global `type` or `interface` preference would contradict that rule, so it stays disabled. Review the distinction manually. `typescript/no-floating-promises` and `typescript/await-thenable` remain optional: both require `oxlint-tsgolint` and `options.typeAware: true`.
591
600
 
592
601
  Match `.oxfmtrc.json`: 100 columns, double quotes, semicolons, trailing commas and built-in import sorting.
593
602
 
@@ -619,7 +628,7 @@ Before any change ships:
619
628
 
620
629
  - [ ] Every untrusted input has a `@warlock.js/seal` schema at the boundary
621
630
  - [ ] Money fields are integer minor units, time stored as UTC
622
- - [ ] IDs exposed in URLs are opaque (UUID / nanoid)
631
+ - [ ] IDs exposed in URLs are opaque (the starter uses a separate public UUID)
623
632
  - [ ] Every external call (HTTP, AI, queue, DB) has an explicit timeout
624
633
  - [ ] External side effects (payments, emails, webhooks) use idempotency keys
625
634
  - [ ] Response shape matches the project's canonical envelope
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: data-and-persistence
3
- description: 'How this project models, stores, and migrates data with `@warlock.js/cascade` — money as integer minor units (`total_cents`, `amount_cents`) with `currency` alongside, time as UTC `Date` columns named `<verb>_at` (e.g. `synced_at`, `checked_out_at`, `abandoned_at`), opaque IDs in URLs (UUID / nanoid), audit columns auto-managed by the framework (`created_at` / `updated_at`), soft-delete via cascade''s delete strategy (`@warlock.js/cascade/configure-delete-strategy/SKILL.md`), schemas defined with `v.object` and inferred into `Infer<>` types, models declared via `@RegisterModel()` + `Model<Schema>` + typed getters (`this.get<T>(key, default)`), migrations via `Migration.create(Model, {...columns}, { unique })` with column types `text() / integer() / double() / bool() / json() / arrayText()`, relations via `@BelongsTo("Name")` and `@HasMany("Name")`, snake_case columns + camelCase getters. Triggers: defining a model / schema / cascade blueprint; writing a migration; choosing a column type for currency, prices, totals, balances, refunds, fees; choosing how to store a timestamp / date; deciding ID format; adding audit columns; designing soft-delete vs hard-delete; defining relations (BelongsTo, HasMany); user asks "how do we store money", "UTC vs local time", "what ID format do we use", "should this be soft-deleted", "migration rules", "BelongsTo vs HasMany", "what does the model look like", "how do I add a column". Skip: API response shape (load `skills/api-design/SKILL.md`); query performance / N+1 / caching (load `skills/observability-and-resilience/SKILL.md`); pure business-logic refactors with no schema change; framework primitive deep-dive — load the relevant `@warlock.js/cascade/*` skill (define-model, paginate-results, configure-delete-strategy, etc.).'
3
+ description: "Project persistence conventions: money, UTC dates, public UUIDs, Cascade models, migrations, indexes, relations and delete strategies. Use when changing stored data or model schemas. Use api-design instead for HTTP envelopes and resource output."
4
4
  ---
5
5
 
6
6
  # Data & persistence
@@ -8,7 +8,7 @@ description: 'How this project models, stores, and migrates data with `@warlock.
8
8
  **Status:** Stable
9
9
  **Applies to:** Every model, schema, migration, and persisted entity in `src/app/**`.
10
10
 
11
- How we store data so it survives a year of new requirements, a currency conversion bug, and a timezone-aware feature request. Framework mechanics live in `@warlock.js/cascade/skills/*` — this skill is the **project-level conventions** layered on top.
11
+ How we store data so it survives a year of new requirements, a currency conversion bug, and a timezone-aware feature request. Framework mechanics live in the `@warlock.js/cascade` skill — this skill is the **project-level conventions** layered on top.
12
12
 
13
13
  > **Sub-agent rule:** Before writing any model, schema, or migration, read this file.
14
14
 
@@ -19,17 +19,17 @@ How we store data so it survives a year of new requirements, a currency conversi
19
19
  ### 1.1 The rule
20
20
 
21
21
  - Store money as **integers in the currency's smallest unit** — cents for USD, halalas for SAR.
22
- - Column name carries the unit: `total_cents`, `amount_cents`, `fee_cents`, `discount_cents`.
22
+ - Column name carries the unit: `totalCents`, `amountCents`, `feeCents`, `discountCents`.
23
23
  - A `currency` column lives next to every money column (or once per row if the entity is mono-currency).
24
24
 
25
- The Cart model is the project's canonical example:
25
+ For example:
26
26
 
27
27
  ```typescript
28
- // ✅ from cart.model.ts
28
+ // ✅ example cart schema
29
29
  export const cartSchema = v.object({
30
30
  /* ... */
31
- total_cents: v.number().default(0),
32
- total_items: v.number().default(0),
31
+ totalCents: v.int().default(0),
32
+ totalItems: v.int().default(0),
33
33
  currency: v.string().default("USD"),
34
34
  /* ... */
35
35
  });
@@ -38,19 +38,19 @@ export const cartSchema = v.object({
38
38
  ### 1.2 Why never floats
39
39
 
40
40
  ```typescript
41
- 0.1 + 0.2 === 0.3 // false
42
- 1234.567 * 100 // 123456.69999999999
41
+ 0.1 + 0.2 === 0.3; // false
42
+ 1234.567 * 100; // 123456.69999999999
43
43
  ```
44
44
 
45
45
  Compound a million orders' subtotals across a payout report and you'll be off by enough to matter. Integer cents prevents this entire class of bug.
46
46
 
47
47
  ### 1.3 The AI-pricing exception
48
48
 
49
- `ai-models.input_price` and `ai-models.output_price` are stored as `v.number()` — not cents. This is **deliberate**: AI provider pricing is sub-cent per token (e.g. $0.000002), where integer minor units round to zero. The exception is *only* AI / token pricing; business money (orders, products, payments, balances, refunds) follows the cents rule.
49
+ A token-pricing model may use a documented decimal or scaled sub-cent unit: AI provider pricing is sub-cent per token (e.g. $0.000002), where integer minor units round to zero. The exception is _only_ AI / token pricing; business money (orders, products, payments, balances, refunds) follows the cents rule.
50
50
 
51
51
  ### 1.4 Display formatting at the edge
52
52
 
53
- Formatting (`$12.99`, `12.99 SAR`) is the resource layer's job — see `skills/api-design/SKILL.md` § 5. The model and service work in cents; the resource converts.
53
+ Formatting (`$12.99`, `12.99 SAR`) is the resource layer's job — see the `api-design` skill § 5. The model and service work in cents; the resource converts.
54
54
 
55
55
  ---
56
56
 
@@ -61,24 +61,24 @@ Formatting (`$12.99`, `12.99 SAR`) is the resource layer's job — see `skills/a
61
61
  - All timestamp columns store **UTC**, full stop.
62
62
  - The column type is `date()` in the migration; the schema type is `v.date()` returning `Date`.
63
63
 
64
- ### 2.2 Naming — `<verb>_at`
64
+ ### 2.2 Naming — `<verb>At`
65
65
 
66
- Time columns end with `_at` and describe what happened, in past tense:
66
+ Time columns end with `At` and describe what happened, in past tense:
67
67
 
68
- | Column | Meaning |
69
- | --------------- | -------------------------------- |
70
- | `created_at` | row was created |
71
- | `updated_at` | row was last updated |
72
- | `synced_at` | row was synced from a source |
73
- | `checked_out_at`| cart was checked out |
74
- | `abandoned_at` | cart was marked abandoned |
75
- | `expires_at` | row will become invalid |
68
+ | Column | Meaning |
69
+ | -------------- | ---------------------------- |
70
+ | `createdAt` | row was created |
71
+ | `updatedAt` | row was last updated |
72
+ | `syncedAt` | row was synced from a source |
73
+ | `checkedOutAt` | cart was checked out |
74
+ | `abandonedAt` | cart was marked abandoned |
75
+ | `expiresAt` | row will become invalid |
76
76
 
77
77
  ```typescript
78
- // ✅ from cart.model.ts
79
- synced_at: v.date().optional(),
80
- abandoned_at: v.date().optional(),
81
- checked_out_at: v.date().optional(),
78
+ // ✅ example cart schema
79
+ syncedAt: v.date().optional(),
80
+ abandonedAt: v.date().optional(),
81
+ checkedOutAt: v.date().optional(),
82
82
  ```
83
83
 
84
84
  ### 2.3 Conversion happens at the response layer
@@ -100,7 +100,22 @@ The project uses `dayjs` for time arithmetic and formatting (already in dependen
100
100
 
101
101
  ### 3.2 Project default
102
102
 
103
- Cascade generates string IDs by default for new records. Use those as the public identifier. Don't expose `_id` (Mongo) or sequential integers (Postgres serial) in URLs even if they exist internally.
103
+ Cascade defaults to sequential `id` values (a NoSQL-generated integer or the SQL primary key). The `model.uuid` getter only stringifies the primary key; it does not generate a UUID. The starter keeps that internal key for relations and authentication and adds a separate public UUID:
104
+
105
+ ```typescript
106
+ import { randomUUID } from "node:crypto";
107
+ import { v } from "@warlock.js/seal";
108
+
109
+ export const orderSchema = v.object({
110
+ publicId: v
111
+ .string()
112
+ .uuid()
113
+ .default(() => randomUUID()),
114
+ totalCents: v.int(),
115
+ });
116
+ ```
117
+
118
+ Add `publicId: string(36).unique()` in `Migration.create(Order, { ... })`; generate it server-side, and omit it from writable client schemas. A default is applied when the field is absent, not on every update. The resource emits `id: ["publicId", "string"]`. Resolve a validated route ID with `Order.findOne({ publicId: id })`, then write through that model instance. `find(id)` and repository `getCached(id)` still target the internal primary key. For an existing application, backfill a unique UUID per row before requiring this column; changing a resource cast to `"string"` does not make a sequential ID opaque.
104
119
 
105
120
  ### 3.3 Why
106
121
 
@@ -112,7 +127,7 @@ Sequential keys leak business volume (`/orders/42178` tells anyone you've placed
112
127
 
113
128
  ### 4.1 Framework-managed
114
129
 
115
- Cascade auto-adds `created_at` and `updated_at` to every model. You do **not** add them to the schema or migration — the framework handles them.
130
+ Cascade timestamp storage names are driver-dependent: MongoDB defaults to `createdAt` / `updatedAt`, PostgreSQL to `created_at` / `updated_at`. `Migration.create` adds the driver's timestamp columns unless `{ timestamps: false }`; model writes maintain their configured columns. Keep the stored names aligned with the driver or explicitly declare the columns when customizing timestamps. API output keys remain `createdAt` / `updatedAt`: use `["created_at", "date"]` / `["updated_at", "date"]` source-key tuples for a PostgreSQL-backed resource. Do not claim that a camelCase schema key automatically reads a snake_case stored field.
116
131
 
117
132
  ### 4.2 Author tracking
118
133
 
@@ -120,15 +135,15 @@ For entities where "who created this" matters (most business entities), add auth
120
135
 
121
136
  ```typescript
122
137
  // ✅ from ai-model.resource.ts
123
- created_by: "string",
124
- updated_by: "string",
125
- deleted_by: "string",
138
+ createdBy: "string",
139
+ updatedBy: "string",
140
+ deletedBy: "string",
126
141
  ```
127
142
 
128
143
  These store the user-id of the actor at the time of the action. Wire them in the service:
129
144
 
130
145
  ```typescript
131
- await ordersRepository.create({ ...input, created_by: user.id });
146
+ await ordersRepository.create({ ...input, createdBy: user.get("publicId") });
132
147
  ```
133
148
 
134
149
  ### 4.3 What to audit
@@ -141,7 +156,7 @@ Every entity that's user-mutable: yes. Read-only reference data (countries, curr
141
156
 
142
157
  ### 5.1 Default to soft-delete for user-visible entities
143
158
 
144
- Soft-delete keeps the row, sets `deleted_at` + `deleted_by`. The entity is excluded from default queries but recoverable.
159
+ Soft-delete keeps the row and sets the configured `deletedAt` column. Actor tracking such as `deletedBy` is app-owned. Cascade does not hide deleted rows automatically: add a global `whereNull("deletedAt")` scope or use the repository's active-read methods when you need active-only results.
145
160
 
146
161
  ### 5.2 Hard-delete for ephemeral data
147
162
 
@@ -154,7 +169,7 @@ Use hard-delete for:
154
169
 
155
170
  ### 5.3 Mechanics
156
171
 
157
- Configure the delete strategy at the model level — see `@warlock.js/cascade/configure-delete-strategy/SKILL.md` for the framework's per-model knobs (`paranoid`, `forever`, `cascade`).
172
+ Configure the delete strategy at the model level — see the `@warlock.js/cascade` skill's `configure-delete-strategy` topic for the framework's per-model knobs (`deleteStrategy: "soft"`, `"permanent"`, or `"trash"`; per-call `destroy({ strategy })`).
158
173
 
159
174
  ### 5.4 Restore is admin-only
160
175
 
@@ -170,7 +185,7 @@ Never edit a shipped migration. Once it's on `main`, the only change is a new mi
170
185
 
171
186
  ### 6.2 File location
172
187
 
173
- Migrations live inside the model folder: `src/app/<module>/models/<model>/migrations/<timestamp>-<noun>.migration.ts`. The framework's `yarn cli migrate` discovers them.
188
+ Migrations live inside the model folder: `src/app/<module>/models/<model>/migrations/<timestamp>-<noun>.migration.ts`. The framework's `npx warlock migrate` discovers them.
174
189
 
175
190
  ### 6.3 Definition shape
176
191
 
@@ -183,16 +198,16 @@ export default Migration.create(
183
198
  AiModel,
184
199
  {
185
200
  provider: text().notNullable(),
186
- code: text().notNullable(),
201
+ providerModelId: text().notNullable(),
187
202
  name: text().notNullable(),
188
203
  context: integer().notNullable(),
189
- is_free: bool().notNullable(),
190
- input_price: double().notNullable(),
204
+ isFree: bool().notNullable(),
205
+ inputPrice: double().notNullable(),
191
206
  config: json().nullable(),
192
207
  features: arrayText().nullable(),
193
208
  },
194
209
  {
195
- unique: [{ columns: ["provider", "provider_model_id"] }],
210
+ unique: [{ columns: ["provider", "providerModelId"] }],
196
211
  },
197
212
  );
198
213
  ```
@@ -218,9 +233,9 @@ If a backfill is going to touch millions of rows, gate it behind a feature flag
218
233
 
219
234
  ### 7.1 Every query path has a supporting index
220
235
 
221
- - Foreign keys are indexed by default.
236
+ - Add an index for a frequently queried foreign key; do not assume declaring a relation adds one.
222
237
  - Any column you filter or sort on regularly gets an index.
223
- - Composite indexes match query order (`WHERE org_id = ? AND status = ?` → index `(org_id, status)`).
238
+ - Composite indexes match query order (`WHERE orgId = ? AND status = ?` → index `(orgId, status)`).
224
239
 
225
240
  ### 7.2 Add the index alongside the query
226
241
 
@@ -228,23 +243,23 @@ If a new query is introduced, the index lands in the same PR. Don't push slow qu
228
243
 
229
244
  ### 7.3 Tooling
230
245
 
231
- `yarn db.indexes` runs the framework's index management. Define indexes declaratively in the migration's options.
246
+ Define indexes alongside the migration using column `.index()` / `.unique()` or the migration options. See the `@warlock.js/cascade` skill's `write-migration` topic.
232
247
 
233
248
  ---
234
249
 
235
250
  ## 8. Schema conventions
236
251
 
237
- ### 8.1 Column naming — snake_case
252
+ ### 8.1 Column naming — camelCase
238
253
 
239
- All database column names use `snake_case`: `created_at`, `total_cents`, `provider_model_id`. This matches the wire-format convention in resources.
254
+ New model and database column names use `camelCase`: `createdAt`, `totalCents`, `providerModelId`. Resource output also uses camelCase. For legacy snake_case columns, use source-key tuples in the resource as shown in the `api-design` skill; do not rename a stored column merely by changing the output key.
240
255
 
241
- ### 8.2 Foreign keys — `<noun>_id`
256
+ ### 8.2 Foreign keys — `<noun>Id`
242
257
 
243
- `user_id`, `organization_id`, `cart_id`. Always singular, always `_id` suffix. Cascade's `@BelongsTo("Name")` decorator wires the relation off this column by convention.
258
+ `userId`, `organizationId`, `cartId`. Always singular, always `Id` suffix. Pass an explicit `foreignKey` to `@BelongsTo("Name", { foreignKey: "userId" })` when wiring a relation; match the stored column and the model schema.
244
259
 
245
260
  ### 8.3 Booleans — affirmative
246
261
 
247
- `is_active`, `is_free`, `has_shipped`, `was_refunded`. Never negative (`is_not_deleted`, `is_unverified`) — negative booleans break readability under negation (`!isNotDeleted` reads as a riddle).
262
+ `isActive`, `isFree`, `hasShipped`, `wasRefunded`. Never negative (`isNotDeleted`, `isUnverified`) — negative booleans break readability under negation (`!isNotDeleted` reads as a riddle).
248
263
 
249
264
  ### 8.4 Enums — string in the column, enum in the schema
250
265
 
@@ -272,11 +287,11 @@ src/app/<module>/models/<noun>/
272
287
  <timestamp>-<noun>.migration.ts
273
288
  ```
274
289
 
275
- ### 9.2 The model file shape (Cart is the gold standard)
290
+ ### 9.2 Example model file shape
276
291
 
277
292
  ```typescript
278
293
  // 1. schema first
279
- export const cartSchema = v.object({ /* columns */ });
294
+ export const cartSchema = v.object({/* columns */});
280
295
  export type CartSchema = Infer<typeof cartSchema>;
281
296
 
282
297
  // 2. class with decorator
@@ -286,26 +301,26 @@ export class Cart extends Model<CartSchema> {
286
301
  public static schema = cartSchema;
287
302
 
288
303
  // 3. relations
289
- @BelongsTo("Organization")
304
+ @BelongsTo("Organization", { foreignKey: "organizationId" })
290
305
  public organization?: Organization;
291
306
 
292
- @HasMany("CartItem")
307
+ @HasMany("CartItem", { foreignKey: "cartId" })
293
308
  public items?: CartItem[];
294
309
 
295
310
  // 4. typed getters — one per persisted field accessed from app code
296
311
  public get totalCents() {
297
- return this.get<number>("total_cents", 0);
312
+ return this.get<number>("totalCents", 0);
298
313
  }
299
314
  }
300
315
  ```
301
316
 
302
317
  ### 9.3 Typed getters over `.get<T>("field")` at call sites
303
318
 
304
- This is project policy (see memory `feedback_use_model_getters`). Every column accessed from application code gets a typed getter on the model. One line beats `cart.get<number>("total_cents")` across N call sites.
319
+ This is project policy. Every column accessed from application code gets a typed getter on the model. One line beats `cart.get<number>("totalCents")` across N call sites.
305
320
 
306
321
  ### 9.4 Class-level JSDoc for non-obvious domain logic
307
322
 
308
- The Cart model has a multi-paragraph JSDoc explaining identity ownership, lead linkage, source-of-truth rules, and currency snapshotting. Mirror this for any model whose semantics aren't obvious from the columns alone.
323
+ Explain identity ownership, source-of-truth rules, and persisted snapshots for any model whose semantics are not obvious from its columns.
309
324
 
310
325
  ---
311
326
 
@@ -314,16 +329,16 @@ The Cart model has a multi-paragraph JSDoc explaining identity ownership, lead l
314
329
  Before merging a change that touches a model, schema, or migration:
315
330
 
316
331
  - [ ] Money columns are `integer cents` with `currency` alongside (AI-pricing is the only exception)
317
- - [ ] Time columns end with `_at`, store UTC `Date`
332
+ - [ ] Time columns end with `At`, store UTC `Date`
318
333
  - [ ] No timezone-adjusted timestamps in storage
319
334
  - [ ] IDs exposed in URLs / responses are opaque
320
- - [ ] Audit columns (`created_by`, `updated_by`) wired where author tracking matters
335
+ - [ ] Audit columns (`createdBy`, `updatedBy`) wired where author tracking matters
321
336
  - [ ] Soft-delete strategy chosen deliberately, configured via cascade
322
337
  - [ ] Migration is forward-only, not editing a shipped file
323
338
  - [ ] Backfill is a separate migration / background job from the schema change
324
339
  - [ ] Every new query path has a supporting index
325
- - [ ] Column names snake_case, foreign keys `<noun>_id`
326
- - [ ] Booleans are affirmative (`is_active`, never `is_not_deleted`)
340
+ - [ ] Column names camelCase, foreign keys `<noun>Id`
341
+ - [ ] Booleans are affirmative (`isActive`, never `isNotDeleted`)
327
342
  - [ ] Enums defined once in `types/`, never repeated as string literals
328
343
  - [ ] Defaults declared in the schema, not only in the migration
329
344
  - [ ] Every accessed column has a typed getter on the model
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: git-workflow
3
- description: 'Git, branching, commit messages, PRs, and CI gates for this project — conventional commits with module-scoped types (`feat(orders): ...`, `fix(auth): ...`, `refactor(cart): ...`, `docs(users): ...`, `chore: ...`), branch naming (`feat/`, `fix/`, `chore/`, `refactor/`, `docs/` prefix + slug), PR size cap (~400 LoC), required reviewers, CI gates (`yarn tsc` / `yarn lint` / `yarn test` / `yarn audit --level=high`), no force-push to `main`, squash-merge policy, tagging and releases. Triggers: writing a commit message; opening / reviewing / merging a PR; naming a branch; setting up CI; user asks "commit format", "conventional commits", "branch naming", "how big should a PR be", "what CI gates do we need", "how do we release", "can I force push", "squash or merge", "scope in commit message", "what scopes are valid". Skip: code style inside files (load `skills/code-standards/SKILL.md`); deploy mechanics / pm2 setup; framework primitive questions; publishing / semantic-versioning of npm packages.'
3
+ description: "Project Git conventions, branches, conventional commits, pull requests, review and CI gates. Use when preparing or reviewing commits and PRs. Use code-standards instead for implementation style."
4
4
  ---
5
5
 
6
6
  # Git workflow
@@ -200,14 +200,14 @@ A drive-by approve is worse than no approve — it gives false confidence. If yo
200
200
 
201
201
  Every PR runs these. All must pass before merge. The exact commands match `package.json` scripts:
202
202
 
203
- | Gate | Command | Blocks on |
204
- | ---------------------------- | ------------------------- | ------------------------------- |
205
- | Type-check | `yarn tsc` | Any TS error |
206
- | Lint | `yarn lint` | Any Oxlint error |
207
- | Tests | `yarn test --run` | Any failed test |
208
- | Coverage (recommended floor) | `yarn test:coverage` | TBD per team policy |
209
- | Dependency audit | `yarn audit --level=high` | High / critical vulnerabilities |
210
- | Format check | `yarn format --check` | Any unformatted file |
203
+ | Gate | Command | Blocks on |
204
+ | ---------------------------- | ------------------------------ | ------------------------------- |
205
+ | Type-check | `npm run tsc` | Any TS error |
206
+ | Lint | `npm run lint:check` | Any Oxlint error |
207
+ | Tests | `npm run test -- --run` | Any failed test |
208
+ | Coverage (recommended floor) | `npm run test:coverage` | TBD per team policy |
209
+ | Dependency audit | `npm audit --audit-level=high` | High / critical vulnerabilities |
210
+ | Format check | `npm run format:check` | Any unformatted file |
211
211
 
212
212
  Additional rules:
213
213
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: module-boundaries
3
- description: 'Architecture rules for how modules under `src/app/**` relate — each module owns one domain noun (`orders`, `users`, `carts`, `payments`); standard layout per module (`controllers/`, `services/`, `models/<noun>/`, `repositories/`, `schema/`, `resources/`, `types/`, `utils/`, `events/`, `errors/`, `routes.ts`); public surface exposed through sub-folder barrels (`services/index.ts`, `utils/index.ts`, `types/index.ts`) — NEVER a module-root `index.ts` re-exporting everything; no module reaches into another module''s internal files; zero circular dependencies between modules; inter-module communication is explicit — direct service call OR `@mongez/events` bus, never shared mutable globals; cross-cutting helpers live in `src/app/shared/`. Triggers: adding a new module under `src/app/**`; importing from another module; deciding where new code belongs; circular import error; Oxlint `import/no-restricted-paths` violation; user asks "can module A use module B''s internals", "how should modules talk", "where should this code live", "I''m getting a circular dependency", "what folders does a module need", "what goes in shared". Skip: intra-module file organization (load `skills/code-standards/SKILL.md` § 4); barrel mechanics within a module (load `skills/code-standards/SKILL.md` § 2.2); deploying / packaging / monorepo questions; framework primitive details — load relevant `@warlock.js/*` skill.'
3
+ description: "Domain module ownership, dependency direction, app sub-folder exports, cross-module services and events, and shared helpers. Use when adding modules or changing imports between domains. Use code-standards instead for organization within one module."
4
4
  ---
5
5
 
6
6
  # Module boundaries
@@ -94,7 +94,7 @@ This is the one absolute rule:
94
94
  ✅ src/app/orders/types/index.ts ← good
95
95
  ```
96
96
 
97
- See `skills/code-standards/SKILL.md` § 2.2 for the reasoning.
97
+ See the `code-standards` skill § 2.2 for the reasoning.
98
98
 
99
99
  ---
100
100