create-warlock 4.2.5 → 4.2.7

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 (96) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/create-app.js +5 -0
  3. package/package.json +2 -2
  4. package/templates/warlock/.env.example +36 -0
  5. package/templates/warlock/.gitattributes +1 -0
  6. package/templates/warlock/.husky/pre-commit +4 -0
  7. package/templates/warlock/.prettierignore +4 -0
  8. package/templates/warlock/.prettierrc.json +10 -0
  9. package/templates/warlock/.vscode/settings.json +41 -0
  10. package/templates/warlock/README.md +57 -0
  11. package/templates/warlock/_.gitignore +6 -0
  12. package/templates/warlock/docs/new-module.md +551 -0
  13. package/templates/warlock/eslint.config.js +98 -0
  14. package/templates/warlock/package.json +74 -0
  15. package/templates/warlock/public/home.css +523 -0
  16. package/templates/warlock/skills/api-design/SKILL.md +461 -0
  17. package/templates/warlock/skills/code-standards/SKILL.md +595 -0
  18. package/templates/warlock/skills/data-and-persistence/SKILL.md +330 -0
  19. package/templates/warlock/skills/git-workflow/SKILL.md +282 -0
  20. package/templates/warlock/skills/module-boundaries/SKILL.md +283 -0
  21. package/templates/warlock/skills/observability-and-resilience/SKILL.md +306 -0
  22. package/templates/warlock/skills/security-baseline/SKILL.md +352 -0
  23. package/templates/warlock/skills/testing-strategy/SKILL.md +323 -0
  24. package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +28 -0
  25. package/templates/warlock/src/app/auth/controllers/login.controller.ts +22 -0
  26. package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +16 -0
  27. package/templates/warlock/src/app/auth/controllers/logout.controller.ts +16 -0
  28. package/templates/warlock/src/app/auth/controllers/me.controller.ts +13 -0
  29. package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +29 -0
  30. package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +23 -0
  31. package/templates/warlock/src/app/auth/main.ts +9 -0
  32. package/templates/warlock/src/app/auth/models/otp/index.ts +1 -0
  33. package/templates/warlock/src/app/auth/models/otp/migrations/22-12-2025_10-30-20.otp-migration.ts +30 -0
  34. package/templates/warlock/src/app/auth/models/otp/otp.model.ts +69 -0
  35. package/templates/warlock/src/app/auth/requests/guarded.request.ts +10 -0
  36. package/templates/warlock/src/app/auth/routes.ts +22 -0
  37. package/templates/warlock/src/app/auth/schema/login.schema.ts +8 -0
  38. package/templates/warlock/src/app/auth/schema/reset-password.schema.ts +9 -0
  39. package/templates/warlock/src/app/auth/services/auth.service.ts +66 -0
  40. package/templates/warlock/src/app/auth/services/forgot-password.service.ts +28 -0
  41. package/templates/warlock/src/app/auth/services/otp.service.ts +173 -0
  42. package/templates/warlock/src/app/auth/services/reset-password.service.ts +39 -0
  43. package/templates/warlock/src/app/auth/utils/auth-error-code.ts +6 -0
  44. package/templates/warlock/src/app/auth/utils/locales.ts +89 -0
  45. package/templates/warlock/src/app/auth/utils/types.ts +14 -0
  46. package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +21 -0
  47. package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +30 -0
  48. package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +15 -0
  49. package/templates/warlock/src/app/posts/models/post/post.model.ts +23 -0
  50. package/templates/warlock/src/app/posts/resources/post.resource.ts +14 -0
  51. package/templates/warlock/src/app/posts/routes.ts +8 -0
  52. package/templates/warlock/src/app/posts/schema/create-post.schema.ts +9 -0
  53. package/templates/warlock/src/app/posts/schema/update-post.schema.ts +9 -0
  54. package/templates/warlock/src/app/shared/components/HomePageComponent.tsx +229 -0
  55. package/templates/warlock/src/app/shared/controllers/home-page.controller.ts +18 -0
  56. package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +8 -0
  57. package/templates/warlock/src/app/shared/routes.ts +4 -0
  58. package/templates/warlock/src/app/shared/services/scheduler.service.ts +3 -0
  59. package/templates/warlock/src/app/shared/tests/infrastructure.test.ts +22 -0
  60. package/templates/warlock/src/app/shared/utils/global-columns-schema.ts +8 -0
  61. package/templates/warlock/src/app/shared/utils/locales.ts +766 -0
  62. package/templates/warlock/src/app/shared/utils/router.ts +30 -0
  63. package/templates/warlock/src/app/uploads/controllers/fetch-uploaded-file.controller.ts +33 -0
  64. package/templates/warlock/src/app/uploads/routes.ts +4 -0
  65. package/templates/warlock/src/app/users/commands/hello-world.command.ts +8 -0
  66. package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +27 -0
  67. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +12 -0
  68. package/templates/warlock/src/app/users/events/inject-created-by-user.into-model.event.ts +32 -0
  69. package/templates/warlock/src/app/users/events/sync.ts +5 -0
  70. package/templates/warlock/src/app/users/main.ts +5 -0
  71. package/templates/warlock/src/app/users/models/user/index.ts +1 -0
  72. package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +15 -0
  73. package/templates/warlock/src/app/users/models/user/user.model.ts +64 -0
  74. package/templates/warlock/src/app/users/repositories/users.repository.ts +23 -0
  75. package/templates/warlock/src/app/users/resources/user.resource.ts +14 -0
  76. package/templates/warlock/src/app/users/routes.ts +8 -0
  77. package/templates/warlock/src/app/users/schema/create-user.schema.ts +11 -0
  78. package/templates/warlock/src/app/users/seeds/users.seed.ts +21 -0
  79. package/templates/warlock/src/app/users/services/get-users.service.ts +5 -0
  80. package/templates/warlock/src/app/users/services/list-users.service.ts +17 -0
  81. package/templates/warlock/src/app/users/services/login-social.ts +19 -0
  82. package/templates/warlock/src/config/app.ts +12 -0
  83. package/templates/warlock/src/config/auth.ts +20 -0
  84. package/templates/warlock/src/config/cache.ts +59 -0
  85. package/templates/warlock/src/config/database.ts +65 -0
  86. package/templates/warlock/src/config/http.ts +23 -0
  87. package/templates/warlock/src/config/log.ts +22 -0
  88. package/templates/warlock/src/config/mail.ts +16 -0
  89. package/templates/warlock/src/config/repository.ts +11 -0
  90. package/templates/warlock/src/config/storage.ts +34 -0
  91. package/templates/warlock/src/config/tests.ts +5 -0
  92. package/templates/warlock/src/config/validation.ts +7 -0
  93. package/templates/warlock/storage/.gitignore +2 -0
  94. package/templates/warlock/tsconfig.json +27 -0
  95. package/templates/warlock/warlock.config.ts +15 -0
  96. package/templates/warlock/yarn.lock +2332 -0
@@ -0,0 +1,330 @@
1
+ ---
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.).'
4
+ ---
5
+
6
+ # Data & persistence
7
+
8
+ **Status:** Stable
9
+ **Applies to:** Every model, schema, migration, and persisted entity in `src/app/**`.
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.
12
+
13
+ > **Sub-agent rule:** Before writing any model, schema, or migration, read this file.
14
+
15
+ ---
16
+
17
+ ## 1. Money — integer minor units, always
18
+
19
+ ### 1.1 The rule
20
+
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`.
23
+ - A `currency` column lives next to every money column (or once per row if the entity is mono-currency).
24
+
25
+ The Cart model is the project's canonical example:
26
+
27
+ ```typescript
28
+ // ✅ from cart.model.ts
29
+ export const cartSchema = v.object({
30
+ /* ... */
31
+ total_cents: v.number().default(0),
32
+ total_items: v.number().default(0),
33
+ currency: v.string().default("USD"),
34
+ /* ... */
35
+ });
36
+ ```
37
+
38
+ ### 1.2 Why never floats
39
+
40
+ ```typescript
41
+ 0.1 + 0.2 === 0.3 // false
42
+ 1234.567 * 100 // 123456.69999999999
43
+ ```
44
+
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
+
47
+ ### 1.3 The AI-pricing exception
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.
50
+
51
+ ### 1.4 Display formatting at the edge
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.
54
+
55
+ ---
56
+
57
+ ## 2. Time — UTC at rest, convert at the edge
58
+
59
+ ### 2.1 Storage
60
+
61
+ - All timestamp columns store **UTC**, full stop.
62
+ - The column type is `date()` in the migration; the schema type is `v.date()` returning `Date`.
63
+
64
+ ### 2.2 Naming — `<verb>_at`
65
+
66
+ Time columns end with `_at` and describe what happened, in past tense:
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 |
76
+
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(),
82
+ ```
83
+
84
+ ### 2.3 Conversion happens at the response layer
85
+
86
+ Services and models work in UTC `Date` objects. Timezone conversion (UTC → user's local timezone) happens once, at the resource / response layer, using the authenticated user's preference. Never store timezone-adjusted timestamps.
87
+
88
+ ### 2.4 Date library
89
+
90
+ The project uses `dayjs` for time arithmetic and formatting (already in dependencies). Never `moment`, never hand-rolled UTC math. The framework's `@mongez/time-wizard` provides typed helpers on top of dayjs — prefer it where available.
91
+
92
+ ---
93
+
94
+ ## 3. IDs
95
+
96
+ ### 3.1 The rule
97
+
98
+ - **Internal primary keys** may be sequential — they're cheap, indexed, and never leave the database.
99
+ - **Any ID that crosses a process boundary** (URL, response body, log line, webhook) is opaque — UUID or nanoid.
100
+
101
+ ### 3.2 Project default
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.
104
+
105
+ ### 3.3 Why
106
+
107
+ Sequential keys leak business volume (`/orders/42178` tells anyone you've placed ~42k orders) and invite enumeration attacks (incrementing the path to find someone else's resource). Opaque IDs prevent both.
108
+
109
+ ---
110
+
111
+ ## 4. Audit columns
112
+
113
+ ### 4.1 Framework-managed
114
+
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.
116
+
117
+ ### 4.2 Author tracking
118
+
119
+ For entities where "who created this" matters (most business entities), add author columns to the resource:
120
+
121
+ ```typescript
122
+ // ✅ from ai-model.resource.ts
123
+ created_by: "string",
124
+ updated_by: "string",
125
+ deleted_by: "string",
126
+ ```
127
+
128
+ These store the user-id of the actor at the time of the action. Wire them in the service:
129
+
130
+ ```typescript
131
+ await ordersRepository.create({ ...input, created_by: user.id });
132
+ ```
133
+
134
+ ### 4.3 What to audit
135
+
136
+ Every entity that's user-mutable: yes. Read-only reference data (countries, currencies, system enums): no — audit columns are noise there.
137
+
138
+ ---
139
+
140
+ ## 5. Soft-delete vs hard-delete
141
+
142
+ ### 5.1 Default to soft-delete for user-visible entities
143
+
144
+ Soft-delete keeps the row, sets `deleted_at` + `deleted_by`. The entity is excluded from default queries but recoverable.
145
+
146
+ ### 5.2 Hard-delete for ephemeral data
147
+
148
+ Use hard-delete for:
149
+
150
+ - Sessions, OTPs, refresh tokens (expired = gone)
151
+ - Cache entries
152
+ - Idempotency-key records past their TTL
153
+ - GDPR right-to-erasure requests (when triggered)
154
+
155
+ ### 5.3 Mechanics
156
+
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`).
158
+
159
+ ### 5.4 Restore is admin-only
160
+
161
+ If an entity supports restore, expose it through an admin endpoint, never end-user UI (unless the feature is explicitly "trash / undelete" — a deliberate UX, not an accident).
162
+
163
+ ---
164
+
165
+ ## 6. Migrations
166
+
167
+ ### 6.1 Forward-only
168
+
169
+ Never edit a shipped migration. Once it's on `main`, the only change is a new migration on top.
170
+
171
+ ### 6.2 File location
172
+
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.
174
+
175
+ ### 6.3 Definition shape
176
+
177
+ ```typescript
178
+ // ✅ from ai-model.migration.ts
179
+ import { arrayText, bool, double, integer, json, Migration, text } from "@warlock.js/cascade";
180
+ import { AiModel } from "../ai-model.model";
181
+
182
+ export default Migration.create(
183
+ AiModel,
184
+ {
185
+ provider: text().notNullable(),
186
+ code: text().notNullable(),
187
+ name: text().notNullable(),
188
+ context: integer().notNullable(),
189
+ is_free: bool().notNullable(),
190
+ input_price: double().notNullable(),
191
+ config: json().nullable(),
192
+ features: arrayText().nullable(),
193
+ },
194
+ {
195
+ unique: [{ columns: ["provider", "provider_model_id"] }],
196
+ },
197
+ );
198
+ ```
199
+
200
+ Column type helpers: `text()`, `integer()`, `double()`, `bool()`, `json()`, `arrayText()`, `date()`. Always declare `.notNullable()` or `.nullable()` explicitly — never leave nullability implicit.
201
+
202
+ ### 6.4 Backfill separately from schema change
203
+
204
+ Two migrations, not one:
205
+
206
+ 1. Schema change (add column nullable, or add column with default).
207
+ 2. Backfill (populate the column from existing data).
208
+
209
+ This keeps each migration short, fast, and safe to revert in stages.
210
+
211
+ ### 6.5 Long-running backfills
212
+
213
+ If a backfill is going to touch millions of rows, gate it behind a feature flag and run it as a background job — not inside the migration runner. The migration just creates the schema; the backfill is application code.
214
+
215
+ ---
216
+
217
+ ## 7. Indexes
218
+
219
+ ### 7.1 Every query path has a supporting index
220
+
221
+ - Foreign keys are indexed by default.
222
+ - 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)`).
224
+
225
+ ### 7.2 Add the index alongside the query
226
+
227
+ If a new query is introduced, the index lands in the same PR. Don't push slow queries to production and patch later.
228
+
229
+ ### 7.3 Tooling
230
+
231
+ `yarn db.indexes` runs the framework's index management. Define indexes declaratively in the migration's options.
232
+
233
+ ---
234
+
235
+ ## 8. Schema conventions
236
+
237
+ ### 8.1 Column naming — snake_case
238
+
239
+ All database column names use `snake_case`: `created_at`, `total_cents`, `provider_model_id`. This matches the wire-format convention in resources.
240
+
241
+ ### 8.2 Foreign keys — `<noun>_id`
242
+
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.
244
+
245
+ ### 8.3 Booleans — affirmative
246
+
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).
248
+
249
+ ### 8.4 Enums — string in the column, enum in the schema
250
+
251
+ ```typescript
252
+ status: v.enum(CartStatus).default(CartStatus.ACTIVE),
253
+ ```
254
+
255
+ The enum is defined once in `app/<module>/types/<noun>-status.type.ts` and referenced everywhere. Never use raw string literals for status fields outside the type definition.
256
+
257
+ ### 8.5 Defaults
258
+
259
+ Declare defaults in the schema (`v.string().default("USD")`), not just in the migration. The schema default keeps `Infer<>` clean and gives the service a sensible starting value.
260
+
261
+ ---
262
+
263
+ ## 9. Models — structural rules
264
+
265
+ ### 9.1 File layout
266
+
267
+ ```
268
+ src/app/<module>/models/<noun>/
269
+ <noun>.model.ts ← @RegisterModel() + schema + class + getters
270
+ index.ts ← re-export
271
+ migrations/
272
+ <timestamp>-<noun>.migration.ts
273
+ ```
274
+
275
+ ### 9.2 The model file shape (Cart is the gold standard)
276
+
277
+ ```typescript
278
+ // 1. schema first
279
+ export const cartSchema = v.object({ /* columns */ });
280
+ export type CartSchema = Infer<typeof cartSchema>;
281
+
282
+ // 2. class with decorator
283
+ @RegisterModel()
284
+ export class Cart extends Model<CartSchema> {
285
+ public static table = "carts";
286
+ public static schema = cartSchema;
287
+
288
+ // 3. relations
289
+ @BelongsTo("Organization")
290
+ public organization?: Organization;
291
+
292
+ @HasMany("CartItem")
293
+ public items?: CartItem[];
294
+
295
+ // 4. typed getters — one per persisted field accessed from app code
296
+ public get totalCents() {
297
+ return this.get<number>("total_cents", 0);
298
+ }
299
+ }
300
+ ```
301
+
302
+ ### 9.3 Typed getters over `.get<T>("field")` at call sites
303
+
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.
305
+
306
+ ### 9.4 Class-level JSDoc for non-obvious domain logic
307
+
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.
309
+
310
+ ---
311
+
312
+ ## 10. Review checklist
313
+
314
+ Before merging a change that touches a model, schema, or migration:
315
+
316
+ - [ ] Money columns are `integer cents` with `currency` alongside (AI-pricing is the only exception)
317
+ - [ ] Time columns end with `_at`, store UTC `Date`
318
+ - [ ] No timezone-adjusted timestamps in storage
319
+ - [ ] IDs exposed in URLs / responses are opaque
320
+ - [ ] Audit columns (`created_by`, `updated_by`) wired where author tracking matters
321
+ - [ ] Soft-delete strategy chosen deliberately, configured via cascade
322
+ - [ ] Migration is forward-only, not editing a shipped file
323
+ - [ ] Backfill is a separate migration / background job from the schema change
324
+ - [ ] 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`)
327
+ - [ ] Enums defined once in `types/`, never repeated as string literals
328
+ - [ ] Defaults declared in the schema, not only in the migration
329
+ - [ ] Every accessed column has a typed getter on the model
330
+ - [ ] Class-level JSDoc on the model if domain semantics aren't obvious
@@ -0,0 +1,282 @@
1
+ ---
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.'
4
+ ---
5
+
6
+ # Git workflow
7
+
8
+ **Status:** Stable
9
+ **Applies to:** Every commit, branch, and pull request in the project.
10
+
11
+ The process layer that lets a team of N work without stepping on each other and lets a reader six months later understand why a line is there.
12
+
13
+ > **Sub-agent rule:** Before opening a commit or PR, read this file.
14
+
15
+ ---
16
+
17
+ ## 1. Commit messages — conventional commits
18
+
19
+ ### 1.1 Format
20
+
21
+ ```
22
+ <type>(<scope>): <imperative summary>
23
+
24
+ [optional body — explains the WHY when the diff doesn't]
25
+
26
+ [optional footer — BREAKING CHANGE, ticket refs, co-authors]
27
+ ```
28
+
29
+ Examples:
30
+
31
+ ```
32
+ feat(orders): add cancellation flow with refund + audit trail
33
+ fix(auth): reject expired refresh tokens on rotation
34
+ docs(users): document the profile-update endpoint
35
+ refactor(cart): extract totals calculation into a util
36
+ ```
37
+
38
+ ### 1.2 Types
39
+
40
+ | Type | Use for |
41
+ | ----------- | ------------------------------------------------ |
42
+ | `feat` | New user-visible feature or capability |
43
+ | `fix` | Bug fix |
44
+ | `refactor` | Internal restructure with no behaviour change |
45
+ | `docs` | Documentation only (skills, README, code docs) |
46
+ | `test` | Adding or updating tests only |
47
+ | `chore` | Tooling, config, scripts — no app code change |
48
+ | `perf` | Performance improvement |
49
+ | `build` | Build system or external dependencies |
50
+ | `ci` | CI configuration |
51
+ | `style` | Formatting only (rare — Prettier handles this) |
52
+
53
+ ### 1.3 Scope
54
+
55
+ The scope identifies what area of the codebase changed. Use:
56
+
57
+ - The module name for app-level changes: `feat(orders): ...`, `fix(auth): ...`
58
+ - Omit when the change is genuinely cross-cutting: `chore: bump node version`
59
+
60
+ ### 1.4 Summary
61
+
62
+ - Imperative present tense: "add" not "added", "fix" not "fixed"
63
+ - Under 72 characters
64
+ - No trailing period
65
+ - Lowercase first word (after the type / scope)
66
+
67
+ ### 1.5 Body — explain the WHY
68
+
69
+ If the diff doesn't make the reasoning obvious, the body does. One short paragraph is usually enough. Skip the body for trivial changes.
70
+
71
+ ### 1.6 Breaking changes
72
+
73
+ Either:
74
+
75
+ - Add `!` after type: `feat(api)!: rename /users to /accounts`
76
+ - Or include a `BREAKING CHANGE:` footer with details
77
+
78
+ Both work; pick one per project and use it consistently. Project default: footer (more visible in `git log` output).
79
+
80
+ ### 1.7 Ticket references
81
+
82
+ When the change traces to a ticket, reference it in the footer:
83
+
84
+ ```
85
+ feat(orders): add cancellation flow
86
+
87
+ Refs: WAR-1234
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 2. Branch naming
93
+
94
+ ### 2.1 Format
95
+
96
+ `<type>/<short-slug>` — lowercase, hyphenated, no spaces.
97
+
98
+ Examples:
99
+
100
+ ```
101
+ feat/order-cancel-endpoint
102
+ fix/login-rate-limit
103
+ refactor/extract-cart-totals-helper
104
+ docs/skills-code-standards-section-9
105
+ chore/bump-vitest-to-4
106
+ ```
107
+
108
+ ### 2.2 Types match commit types
109
+
110
+ Use the same vocabulary as commits: `feat/`, `fix/`, `refactor/`, `docs/`, `test/`, `chore/`, `perf/`, `ci/`.
111
+
112
+ ### 2.3 Optional ticket prefix
113
+
114
+ When the team uses ticket tracking, `<type>/<ticket-id>-<slug>` is fine:
115
+
116
+ ```
117
+ feat/war-1234-order-cancel-endpoint
118
+ ```
119
+
120
+ ### 2.4 Never work directly on `main`
121
+
122
+ Even one-line doc fixes get a branch. The protected-branch rule (§ 7) makes this enforced, not optional.
123
+
124
+ ---
125
+
126
+ ## 3. Pull requests
127
+
128
+ ### 3.1 Size cap — under 400 lines changed
129
+
130
+ 400 lines of meaningful changes (excluding lockfiles, generated code, snapshot data). Beyond that, the review becomes performative — reviewers skim, real issues slip through.
131
+
132
+ ### 3.2 When it's bigger, split
133
+
134
+ A common shape:
135
+
136
+ 1. **Prep PR** — pure refactor / extraction, no behaviour change. Lands first.
137
+ 2. **Feature PR** — the actual new logic, smaller because the prep already landed.
138
+ 3. **Tests PR** — coverage for the feature.
139
+
140
+ Each PR is independently mergeable, independently reviewable.
141
+
142
+ ### 3.3 Description template
143
+
144
+ ```markdown
145
+ ## What
146
+ One-line summary of the change.
147
+
148
+ ## Why
149
+ The reason — link to a ticket, design doc, or bug report.
150
+
151
+ ## How to verify
152
+ Steps a reviewer can take to confirm it works.
153
+
154
+ ## Screenshots / recordings
155
+ If UI or output changed.
156
+
157
+ ## Notes for reviewer
158
+ Anything that's intentionally weird or worth knowing.
159
+ ```
160
+
161
+ ### 3.4 Self-review first
162
+
163
+ Read your own diff before requesting review. The number of "oh I forgot to remove that" moments you catch yourself saves the reviewer a round-trip and saves your reputation.
164
+
165
+ ### 3.5 Required reviewers
166
+
167
+ - **Default** — 1 reviewer
168
+ - **Security, auth, payments, data migrations** — 2 reviewers, one with domain ownership
169
+
170
+ ---
171
+
172
+ ## 4. Review
173
+
174
+ ### 4.1 Read the diff cover to cover
175
+
176
+ Not just the changed lines — the surrounding context too. A line that looks fine in isolation may be obviously broken in context.
177
+
178
+ ### 4.2 Suggestion vs. blocker — label it
179
+
180
+ - **Blocker**: must change before merge. State it as such.
181
+ - **Suggestion**: nice-to-have, optional, can land later. Mark it explicitly so the author isn't guessing.
182
+ - **Question**: you don't understand something — ask, don't assume.
183
+
184
+ ### 4.3 Approve means "I'd be on call for this"
185
+
186
+ Approval is a load-bearing signature. Approve only if you're comfortable owning the code after the author rotates off the project.
187
+
188
+ ### 4.4 No "LGTM" without reading
189
+
190
+ A drive-by approve is worse than no approve — it gives false confidence. If you don't have time to review properly, say so and let someone else.
191
+
192
+ ---
193
+
194
+ ## 5. CI gates
195
+
196
+ Every PR runs these. All must pass before merge. The exact commands match `package.json` scripts:
197
+
198
+ | Gate | Command | Blocks on |
199
+ | ----------------------------- | ----------------------------- | ---------------------------------- |
200
+ | Type-check | `yarn tsc` | Any TS error |
201
+ | Lint | `yarn lint` | Any ESLint error |
202
+ | Tests | `yarn test --run` | Any failed test |
203
+ | Coverage (recommended floor) | `yarn test:coverage` | TBD per team policy |
204
+ | Dependency audit | `yarn audit --level=high` | High / critical vulnerabilities |
205
+ | Format check | `yarn format --check` | Any unformatted file |
206
+
207
+ Additional rules:
208
+
209
+ - No `.only` / `.skip` in committed tests (lint rule or CI grep).
210
+ - No `console.log` in committed code (lint rule).
211
+ - No `TODO` without a ticket reference (warning, not blocker).
212
+
213
+ ---
214
+
215
+ ## 6. Merging
216
+
217
+ ### 6.1 Squash merge
218
+
219
+ One commit per PR onto `main`. The squashed commit message is the conventional-commit format from § 1, generated from the PR title.
220
+
221
+ This keeps `main`'s history readable — one logical change per commit — while allowing PR branches to have messy work-in-progress commits.
222
+
223
+ ### 6.2 `main` is always green
224
+
225
+ If `main` is red, the team's first job is to make it green. No new PRs merge until it is.
226
+
227
+ ### 6.3 Hotfix flow
228
+
229
+ Same conventional commits + same CI gates + expedited review (1 reviewer, can be self-review for true emergencies if a maintainer is on the call). Hotfix PRs are still squash-merged.
230
+
231
+ ---
232
+
233
+ ## 7. Protected branches
234
+
235
+ `main` is protected:
236
+
237
+ - No direct push.
238
+ - No force-push.
239
+ - Required CI checks pass before merge.
240
+ - Required reviewer count met.
241
+ - Linear history (squash merges only — no merge commits).
242
+
243
+ These are enforced in the repo settings, not just on the honour system.
244
+
245
+ ---
246
+
247
+ ## 8. Tags and releases
248
+
249
+ ### 8.1 Versioning
250
+
251
+ For a single-monolith app (most templates), versioning is optional — date-tags work fine if you want them at all.
252
+
253
+ For a library / framework package, semver:
254
+
255
+ ```
256
+ v<major>.<minor>.<patch>
257
+ ```
258
+
259
+ ### 8.2 Changelog
260
+
261
+ Generate from conventional commits — every well-formed history can be turned into a readable changelog automatically. Tools: `changesets`, `conventional-changelog`, or the framework's own release script.
262
+
263
+ ### 8.3 Release notes
264
+
265
+ For user-facing apps, ship release notes alongside the tag — what changed, what to test, any known issues.
266
+
267
+ ---
268
+
269
+ ## 9. Review checklist
270
+
271
+ Before merging:
272
+
273
+ - [ ] Commit message follows `<type>(<scope>): <imperative summary>`
274
+ - [ ] Branch name matches `<type>/<slug>`
275
+ - [ ] PR under ~400 lines changed (or split into staged PRs)
276
+ - [ ] Self-reviewed before requesting review
277
+ - [ ] Description filled (what / why / how to verify)
278
+ - [ ] Required reviewer count met
279
+ - [ ] All CI gates green
280
+ - [ ] No `.only` / `.skip` / `console.log` left in
281
+ - [ ] Squash-merge selected, squashed message in conventional format
282
+ - [ ] `main` stays green after merge