@stonepandastudio/cairn 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +60 -23
  2. package/bin/cairn.js +13 -5
  3. package/lib/doctor/index.js +6 -1
  4. package/lib/init.js +15 -1
  5. package/lib/manifest.js +33 -0
  6. package/lib/render/cli.js +116 -0
  7. package/lib/render/engine.js +148 -0
  8. package/lib/render/index.js +220 -0
  9. package/lib/sync/cli.js +197 -0
  10. package/lib/sync/index.js +249 -0
  11. package/package.json +2 -1
  12. package/presets/EXTRACTION.md +210 -0
  13. package/presets/README.md +86 -0
  14. package/presets/angular/code-guidelines.md +197 -0
  15. package/presets/angular/slots/architect-discussion-topics.md +10 -0
  16. package/presets/angular/slots/architect-mandatory-docs.md +5 -0
  17. package/presets/angular/slots/architect-references.md +4 -0
  18. package/presets/angular/slots/implementation-reference.md +34 -0
  19. package/presets/angular/slots/key-patterns.md +11 -0
  20. package/presets/angular/slots/plan-step-ordering.md +11 -0
  21. package/presets/angular/slots/review-checklist.md +16 -0
  22. package/presets/angular/variants/i18n-external-service.md +11 -0
  23. package/presets/angular/variants/i18n-glossr.md +63 -0
  24. package/presets/core/AGENTS.md +49 -0
  25. package/presets/core/README.md +35 -0
  26. package/presets/core/WORKFLOW.md +56 -0
  27. package/presets/core/agents/architect.md +269 -0
  28. package/presets/core/agents/developer.md +145 -0
  29. package/presets/core/agents/reviewer.md +167 -0
  30. package/presets/core/commands/_stub.md +7 -0
  31. package/presets/core/workflow.json +45 -0
  32. package/presets/drizzle/code-guidelines.md +33 -0
  33. package/presets/drizzle/slots/architect-discussion-topics.md +4 -0
  34. package/presets/drizzle/slots/architect-mandatory-docs.md +4 -0
  35. package/presets/drizzle/slots/implementation-reference.md +17 -0
  36. package/presets/drizzle/slots/key-patterns.md +7 -0
  37. package/presets/drizzle/slots/review-checklist.md +10 -0
  38. package/presets/nestjs/code-guidelines.md +273 -0
  39. package/presets/nestjs/slots/architect-discussion-topics.md +4 -0
  40. package/presets/nestjs/slots/architect-mandatory-docs.md +5 -0
  41. package/presets/nestjs/slots/architect-references.md +5 -0
  42. package/presets/nestjs/slots/implementation-reference.md +45 -0
  43. package/presets/nestjs/slots/key-patterns.md +11 -0
  44. package/presets/nestjs/slots/plan-step-ordering.md +12 -0
  45. package/presets/nestjs/slots/review-checklist.md +12 -0
  46. package/presets/nestjs/variants/validation-class-validator.md +120 -0
  47. package/presets/nestjs/variants/validation-zod.md +194 -0
  48. package/presets/nextjs/code-guidelines.md +45 -0
  49. package/presets/nextjs/slots/architect-discussion-topics.md +5 -0
  50. package/presets/nextjs/slots/architect-mandatory-docs.md +3 -0
  51. package/presets/nextjs/slots/architect-references.md +6 -0
  52. package/presets/nextjs/slots/implementation-reference.md +24 -0
  53. package/presets/nextjs/slots/key-patterns.md +8 -0
  54. package/presets/nextjs/slots/plan-step-ordering.md +11 -0
  55. package/presets/nextjs/slots/review-checklist.md +11 -0
  56. package/presets/react/code-guidelines.md +46 -0
  57. package/presets/react/slots/architect-discussion-topics.md +5 -0
  58. package/presets/react/slots/architect-references.md +5 -0
  59. package/presets/react/slots/implementation-reference.md +26 -0
  60. package/presets/react/slots/key-patterns.md +8 -0
  61. package/presets/react/slots/plan-step-ordering.md +9 -0
  62. package/presets/react/slots/review-checklist.md +10 -0
  63. package/presets/tailwind/code-guidelines.md +28 -0
  64. package/presets/tailwind/slots/implementation-reference.md +8 -0
  65. package/presets/tailwind/slots/key-patterns.md +5 -0
  66. package/presets/tailwind/slots/review-checklist.md +8 -0
  67. package/presets/typeorm/code-guidelines.md +329 -0
  68. package/presets/typeorm/slots/architect-discussion-topics.md +4 -0
  69. package/presets/typeorm/slots/architect-mandatory-docs.md +3 -0
  70. package/presets/typeorm/slots/implementation-reference.md +19 -0
  71. package/presets/typeorm/slots/key-patterns.md +8 -0
  72. package/presets/typeorm/slots/review-checklist.md +8 -0
@@ -0,0 +1,329 @@
1
+ # TypeORM Guidelines
2
+
3
+ <!-- cairn preset: typeorm. Composes onto presets/nestjs. A backend on a different
4
+ ORM takes presets/nestjs without this file. -->
5
+
6
+ ## TypeORM Entities
7
+
8
+ * Use snake_case for database column names: `@Column({ name: 'created_at' })`
9
+ * Use camelCase for entity property names: `createdAt`
10
+ * Always define primary key with `@PrimaryGeneratedColumn()`
11
+ * Use timestamp columns: `@CreateDateColumn()`, `@UpdateDateColumn()`
12
+ * Define relationships properly: `@OneToMany()`, `@ManyToOne()`, `@ManyToMany()`
13
+ * Add indices for frequently queried columns: `@Index()`
14
+ * Set cascade options carefully - avoid unintended deletions
15
+ * Use entity listeners when needed: `@BeforeInsert()`, `@BeforeUpdate()`
16
+
17
+ ## Database Operations
18
+
19
+ **⚠️ MANDATORY RULE**: See the **Query Configuration** section below — this is NOT optional.
20
+
21
+ * Use repository pattern with `@InjectRepository(EntityName)`
22
+ * Use query builders for complex queries
23
+ * Optimize queries with proper joins and select statements
24
+ * Use transactions for multi-step operations: `queryRunner.startTransaction()`
25
+ * Handle concurrent updates with optimistic/pessimistic locking
26
+ * Be aware of N+1 query problems - use eager loading or joins
27
+ * Update database schema via migrations only
28
+ * Follow naming conventions for tables and columns in migrations
29
+ * Update `ai/infrastructure/DATABASE_SCHEMA.md` accordingly after schema changes
30
+ * Avoid loading unnecessary data - select only required fields
31
+ * Always decompose queries with one-to-many relationships
32
+ * Relationship one-to-one can be loaded in same query
33
+ * Store loaded entities in maps (id -> entity) to avoid duplicate loads
34
+ * Prefer to use TypeORM `.find` and `findOne` methods over query builder when possible
35
+ * Store IDs of related entities instead of full entities when possible to reduce memory usage
36
+ * **MUST extract query configurations to separate files in `queries/` folder** — see **Query Configuration** below
37
+
38
+ ## Query Configuration
39
+
40
+ **⚠️ MANDATORY RULE**: Extract TypeORM query configurations into separate files in the `queries/` folder within feature modules. This is NOT optional.
41
+
42
+ **When to extract (ALWAYS)**:
43
+ * ✅ Any `FindManyOptions<Entity>` or `FindOneOptions<Entity>` with relations, select, or complex where clauses
44
+ * ✅ Any query configuration longer than 3-4 lines
45
+ * ✅ Any query that filters by multiple conditions
46
+ * ✅ Any query that loads related entities (one-to-one or joins)
47
+ * ✅ Any query that is or might be reused in multiple methods
48
+
49
+ **What NOT to extract**:
50
+ * ❌ Very simple queries like `{ where: { id, companyId } }` (can stay inline if truly minimal)
51
+ * ❌ One-off queries with no filtering (but prefer extraction for consistency)
52
+
53
+ **Benefits**:
54
+ * Reusable query configurations
55
+ * Easier to test and maintain
56
+ * Keeps service methods clean and focused
57
+ * Centralizes query optimization
58
+ * Makes query logic explicit and documented
59
+
60
+ **Naming Convention** (STRICT):
61
+ * File: `get-[entity]-by-[criteria].query.ts`
62
+ * Constant: `GET_[ENTITY]_BY_[CRITERIA]_QUERY`
63
+ * Example: `get-devices-by-projects.query.ts` exports `GET_DEVICES_BY_PROJECTS_QUERY`
64
+
65
+ **Structure**:
66
+ ```typescript
67
+ // src/feature/siteDevices/queries/get-devices-by-projects.query.ts
68
+ import { FindManyOptions, In } from 'typeorm';
69
+ import { SiteDeviceEntity } from '@feature/siteDevices/entities/site-device.entity';
70
+ import { WithCompanyId } from '@common/types/with-company-id.interface';
71
+
72
+ interface GetDevicesByProjectsQueryProps extends WithCompanyId {
73
+ deviceIds?: string[];
74
+ city?: string;
75
+ }
76
+
77
+ export const GET_DEVICES_BY_PROJECTS_QUERY = ({
78
+ companyId,
79
+ deviceIds,
80
+ city,
81
+ }: GetDevicesByProjectsQueryProps): FindManyOptions<SiteDeviceEntity> => ({
82
+ where: {
83
+ companyId,
84
+ ...(deviceIds && deviceIds.length > 0 ? { id: In(deviceIds) } : {}),
85
+ ...(city ? { site: { city: { name: ILike(`%${city}%`) } } } : {}),
86
+ },
87
+ relations: {
88
+ site: { city: true },
89
+ equipmentType: { icon: true },
90
+ },
91
+ select: {
92
+ id: true,
93
+ description: true,
94
+ site: {
95
+ id: true,
96
+ name: true,
97
+ address: true,
98
+ city: { name: true },
99
+ },
100
+ equipmentType: {
101
+ name: true,
102
+ icon: { id: true, url: true },
103
+ },
104
+ },
105
+ });
106
+ ```
107
+
108
+ **Usage in Service**:
109
+ ```typescript
110
+ // ✅ Correct - use extracted query
111
+ import { GET_DEVICES_BY_PROJECTS_QUERY } from './queries/get-devices-by-projects.query';
112
+
113
+ async getDevicesByProjects(companyId: string, filters: FilterDto) {
114
+ return this.repository.find(
115
+ GET_DEVICES_BY_PROJECTS_QUERY({
116
+ companyId,
117
+ deviceIds: filters.deviceIds,
118
+ city: filters.city,
119
+ })
120
+ );
121
+ }
122
+
123
+ // ❌ Incorrect - inline query configuration
124
+ async getDevicesByProjects(companyId: string, filters: FilterDto) {
125
+ return this.repository.find({
126
+ where: { companyId, ... }, // Don't define queries inline
127
+ relations: { ... },
128
+ select: { ... },
129
+ });
130
+ }
131
+ ```
132
+
133
+ **Common Mistakes to Avoid** (VIOLATIONS REQUIRE REWORK):
134
+ * ❌ DON'T inline any query with relations: `relations: { site: { city: true } }` in service code
135
+ * ❌ DON'T inline any query with select statements: `select: { id: true, name: true, ... }` in service code
136
+ * ❌ DON'T inline any query with complex where clauses in service code
137
+ * ❌ DON'T create long query configs (>3-4 lines) anywhere except in `queries/` files
138
+ * ✅ DO extract to `queries/` folder and import as a constant
139
+ * ✅ DO use extracted queries consistently across all service methods
140
+ * ✅ DO name extracted queries with the `GET_*_QUERY` pattern
141
+
142
+ **Pre-Implementation Checklist**:
143
+ 1. Before writing a service method with database queries, plan the query extractions
144
+ 2. Identify all `find()`, `findOne()` calls that need query configs
145
+ 3. Create query files for each unique configuration
146
+ 4. Import and use the extracted query constants
147
+ 5. Never write inline queries with relations/select in service methods
148
+
149
+ ## QueryBuilder Field Selection
150
+
151
+ **⚠️ MANDATORY**: When using `createQueryBuilder`, always select only the fields needed — never rely on the default `SELECT *` behavior.
152
+
153
+ ### Why it matters
154
+
155
+ `leftJoinAndSelect('tk.namespace', 'namespace')` selects **every column** of the joined entity. On a namespace with 8 columns when you need only 3, you transfer 5 useless columns per row — multiplied by every key in the list. At scale this wastes network bandwidth, DB I/O, and memory.
156
+
157
+ ### Rules
158
+
159
+ **Main entity** — replace the implicit `SELECT *` with an explicit `.select([...])`:
160
+ ```typescript
161
+ // ❌ Fetches all 8 columns of translation_keys including deleted_at
162
+ .createQueryBuilder('tk')
163
+
164
+ // ✅ Fetches only what the serializer actually uses
165
+ .createQueryBuilder('tk')
166
+ .select(['tk.id', 'tk.projectId', 'tk.key', 'tk.description', 'tk.namespaceId', 'tk.createdAt'])
167
+ ```
168
+
169
+ **Joined relations** — use `leftJoin` + `addSelect([...])` instead of `leftJoinAndSelect` when you don't need all columns:
170
+ ```typescript
171
+ // ❌ Fetches all 8 namespace columns
172
+ .leftJoinAndSelect('tk.namespace', 'namespace')
173
+
174
+ // ✅ Fetches only the 3 columns used in the response
175
+ .leftJoin('tk.namespace', 'namespace')
176
+ .addSelect(['namespace.id', 'namespace.name', 'namespace.color'])
177
+ ```
178
+
179
+ **Exception** — `leftJoinAndSelect` is acceptable only when the joined entity has ≤ 4 columns total and all are needed (e.g., a tag table with `id`, `translationKeyId`, `tag`).
180
+
181
+ ### Full example
182
+ ```typescript
183
+ const qb = this.translationKeyRepository
184
+ .createQueryBuilder('tk')
185
+ .select(['tk.id', 'tk.projectId', 'tk.key', 'tk.description', 'tk.namespaceId', 'tk.createdAt'])
186
+ .leftJoin('tk.namespace', 'namespace')
187
+ .addSelect(['namespace.id', 'namespace.name', 'namespace.color'])
188
+ .leftJoinAndSelect('tk.tags', 'tags') // tags has only 3 cols — acceptable
189
+ .where('tk.projectId = :projectId', { projectId })
190
+ .andWhere('tk.companyId = :companyId', { companyId });
191
+ ```
192
+
193
+ ### Checklist before writing a QueryBuilder query
194
+ 1. List every field that the serializer/response DTO actually reads
195
+ 2. Add `.select([...])` with only those fields on the main entity
196
+ 3. For each joined relation, count its columns — if > 4 or not all needed, use `leftJoin` + `addSelect`
197
+ 4. Never use `leftJoinAndSelect` on entities with many columns (namespaces, projects, users, companies)
198
+
199
+ ## Database Query Decomposition Pattern
200
+
201
+ **IMPORTANT**: Always decompose queries with one-to-many or many-to-many relationships into separate queries.
202
+
203
+ **Why**:
204
+ - Avoids Cartesian products from JOINs
205
+ - Reduces data duplication over the wire
206
+ - Better performance for large datasets
207
+ - More explicit control over loaded data
208
+
209
+ **Pattern**:
210
+ 1. Load primary entities with only one-to-one relations
211
+ 2. Extract unique IDs for related entities
212
+ 3. Load related entities in batch queries (using `In()` operator)
213
+ 4. Create Maps for O(1) lookup — see the map rule in **Database Operations**
214
+ 5. Attach related entities to primary entities in memory
215
+
216
+ **Example**:
217
+ ```typescript
218
+ // Step 1: Load devices with only image (one-to-one)
219
+ const devices = await this.deviceRepository.find({
220
+ where: { companyId },
221
+ relations: { image: true }, // ✅ One-to-one OK
222
+ select: { id: true, siteId: true, equipmentTypeId: true, ... }
223
+ });
224
+
225
+ // Step 2: Extract unique IDs
226
+ const siteIds = [...new Set(devices.map(d => d.siteId).filter(Boolean))];
227
+ const equipmentTypeIds = [...new Set(devices.map(d => d.equipmentTypeId).filter(Boolean))];
228
+
229
+ // Step 3: Load related entities in batch
230
+ const sites = await this.siteRepository.find({
231
+ where: { id: In(siteIds) },
232
+ select: { id: true, name: true, cityId: true }
233
+ });
234
+
235
+ const equipmentTypes = await this.equipmentTypeRepository.find({
236
+ where: { id: In(equipmentTypeIds) },
237
+ relations: { icon: true }, // ✅ One-to-one OK
238
+ });
239
+
240
+ // Step 4: Create lookup maps (O(1) access)
241
+ const siteMap = new Map(sites.map(s => [s.id, s]));
242
+ const equipmentTypeMap = new Map(equipmentTypes.map(e => [e.id, e]));
243
+
244
+ // Step 5: Attach in memory
245
+ return devices.map(device => ({
246
+ ...device,
247
+ site: device.siteId ? siteMap.get(device.siteId) : null,
248
+ equipmentType: device.equipmentTypeId ? equipmentTypeMap.get(device.equipmentTypeId) : null,
249
+ }));
250
+ ```
251
+
252
+ **When to use this pattern**:
253
+ - ✅ Loading entities with one-to-many relations (e.g., devices → sites)
254
+ - ✅ Loading entities with many-to-many relations (e.g., devices ↔ projects)
255
+ - ✅ When same related entity appears multiple times (sites, cities)
256
+ - ✅ When related entities need their own related entities (site → city → province)
257
+
258
+ **When NOT to use this pattern**:
259
+ - ❌ Simple one-to-one relations (use `relations` in single query)
260
+ - ❌ Loading single entity by ID
261
+ - ❌ Very small datasets (< 10 records)
262
+
263
+ ## Migrations
264
+
265
+ > **⚠️ CRITICAL — read before touching any migration file.**
266
+ > TypeORM runs migrations in ascending timestamp order. A wrong timestamp silently runs a migration out of sequence, corrupting the schema. This has happened in production. Do not repeat it.
267
+
268
+ ### File naming
269
+
270
+ ```
271
+ {timestamp}-{PascalCaseName}.ts
272
+ ```
273
+
274
+ - **`{timestamp}`** — 13-digit Unix millisecond timestamp captured **at the exact moment the file is created**. Run `Date.now()` in a Node REPL or browser console and copy that value verbatim. Examples of acceptable timestamps: `1749481200000`, `1749523417382`.
275
+ - **`{PascalCaseName}`** — short, descriptive, PascalCase description of what the migration does.
276
+
277
+ **Anti-patterns that are strictly forbidden**:
278
+ - ❌ Round numbers with trailing zeros: `1780700000000`, `1780600000000` — these are fake timestamps, not real `Date.now()` values
279
+ - ❌ Hand-crafting a timestamp to slot between two existing ones (e.g. setting it to `existingTimestamp - 8` to force ordering)
280
+ - ❌ Copying a timestamp from a plan doc, a chat message, or another migration file
281
+ - ❌ Guessing or estimating the current time
282
+
283
+ **Correct examples**:
284
+ ```
285
+ 1749481200000-AddStripePriceId.ts
286
+ 1749512863741-AddInvoicesTable.ts
287
+ 1749523417382-SubscriptionModelRework.ts
288
+ ```
289
+
290
+ ### Ordering guarantee
291
+
292
+ If migration B depends on migration A (e.g. B drops a column A created), A's timestamp **must** be lower than B's. Because timestamps are generated at creation time, the natural order of development guarantees this — as long as you always use real `Date.now()` values. If you fabricate timestamps, you lose this guarantee.
293
+
294
+ ### In plan / description documents
295
+
296
+ Never hardcode a specific timestamp in plan or description files. Write the placeholder instead:
297
+
298
+ ```
299
+ {TIMESTAMP}-MigrationName.ts
300
+ ```
301
+
302
+ Include a note alongside: *"Replace `{TIMESTAMP}` with the output of `Date.now()` at the moment you create the file."*
303
+
304
+ This prevents developers from copying a stale or fabricated timestamp out of a plan doc.
305
+
306
+ ### Generation
307
+
308
+ TypeORM CLI generates a real timestamp automatically:
309
+
310
+ ```bash
311
+ npm run migration:generate -- src/database/migrations/MigrationName
312
+ ```
313
+
314
+ For hand-written migrations: create the file manually, but get the timestamp from `Date.now()` at that moment — never invent one.
315
+
316
+ ## Mappers
317
+
318
+ **When to use mappers**:
319
+ * Complex business logic transformations that go beyond simple field picking
320
+ * Data format conversions (e.g., string to Date, unit conversions)
321
+ * Aggregating data from multiple sources
322
+ * Complex nested structure transformations
323
+
324
+ **When NOT to use mappers**:
325
+ * Simple field selection — the response layer already does this
326
+ * Basic entity-to-DTO conversion — see **Response Transformation** in the nestjs preset
327
+ * Picking a subset of fields
328
+
329
+ Place mapping functions in `mappers/` folder only when truly needed for complex transformations beyond what the response layer provides.
@@ -0,0 +1,4 @@
1
+ <!-- cairn preset: typeorm — appended to presets/nestjs/slots/architect-discussion-topics.md
2
+ on `--stack nestjs,typeorm`. -->
3
+ - **Schema / entity ambiguities** — unclear relationships, nullable vs. required fields, missing constraints, index needs
4
+ - **Migration scope** — what needs a migration vs. a code-only change; destructive changes flagged explicitly
@@ -0,0 +1,3 @@
1
+ <!-- cairn preset: typeorm — appended to presets/nestjs/slots/architect-mandatory-docs.md
2
+ on `--stack nestjs,typeorm`. -->
3
+ - `ai/infrastructure/DATABASE_SCHEMA.md`
@@ -0,0 +1,19 @@
1
+ <!-- cairn preset: typeorm — appended to presets/nestjs/slots/implementation-reference.md
2
+ on `--stack nestjs,typeorm`. -->
3
+
4
+ ### Entities
5
+ - `@Entity()`, `@Column()`, `@PrimaryGeneratedColumn()`, `@CreateDateColumn()`, `@UpdateDateColumn()`
6
+ - snake_case column names (`@Column({ name: 'created_at' })`), camelCase properties
7
+ - Relationships: `@OneToMany()`, `@ManyToOne()`, `@ManyToMany()` with explicit `@JoinColumn()` / `@JoinTable()`
8
+ - `@Index()` where queries filter or sort by a column
9
+ - **Never mutate schema directly** — always generate and run a migration
10
+
11
+ ### Repositories & transactions
12
+ - Inject repositories via `@InjectRepository(Entity)`
13
+ - Wrap multi-step DB operations in a transaction (`queryRunner.startTransaction()`)
14
+ - Complex reads go through a query builder in a `queries/` file, not inline in the service
15
+
16
+ ### Migrations
17
+ - Generate: `npm run migration:generate -- --name=<MigrationName>`
18
+ - Verify the generated SQL before committing — review for accidental destructive changes
19
+ - The DB is the source of truth; never edit entities and skip the migration
@@ -0,0 +1,8 @@
1
+ <!-- cairn preset: typeorm — appended to presets/nestjs/slots/key-patterns.md
2
+ on `--stack nestjs,typeorm`. -->
3
+
4
+ Database schema and entity relationships: **`ai/infrastructure/DATABASE_SCHEMA.md`** —
5
+ keep it current with every migration.
6
+
7
+ TypeORM usage (repository pattern, query builders, transactions, N+1 avoidance):
8
+ **`ai/infrastructure/code-guidelines.md`** § TypeORM.
@@ -0,0 +1,8 @@
1
+ <!-- cairn preset: typeorm — appended to presets/nestjs/slots/review-checklist.md
2
+ when the repo is `--stack nestjs,typeorm`. ORM rules only. -->
3
+
4
+ | No inline TypeORM queries with relations/select — must be in a `queries/` folder | Service files |
5
+ | One-to-many / many-to-many relationships decomposed into separate queries, not JOINs | Service files |
6
+ | Mappers only for complex transformations — simple field picking uses `@Expose()` in the DTO | Mapper files |
7
+ | Schema changes go through a migration, never a direct entity edit + `synchronize` | Entity + migration files |
8
+ | `ai/infrastructure/DATABASE_SCHEMA.md` updated to match a schema change | that file |