cad-workflow 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/bin/cli.js +529 -0
- package/bin/wrapper.js +32 -0
- package/config/install-config.yaml +167 -0
- package/package.json +42 -0
- package/src/base/.cad/config.yaml.tpl +25 -0
- package/src/base/.cad/workflow-status.yaml.tpl +18 -0
- package/src/base/.claude/settings.local.json.tpl +8 -0
- package/src/base/CLAUDE.md +69 -0
- package/src/base/commands/cad.md +547 -0
- package/src/base/commands/commit.md +103 -0
- package/src/base/commands/comprendre.md +96 -0
- package/src/base/commands/concevoir.md +121 -0
- package/src/base/commands/documenter.md +97 -0
- package/src/base/commands/e2e.md +79 -0
- package/src/base/commands/implementer.md +98 -0
- package/src/base/commands/review.md +85 -0
- package/src/base/commands/status.md +55 -0
- package/src/base/skills/clean-code/SKILL.md +92 -0
- package/src/base/skills/tdd/SKILL.md +132 -0
- package/src/integrations/jira/.mcp.json.tpl +19 -0
- package/src/integrations/jira/commands/jira-setup.md +34 -0
- package/src/stacks/backend-only/agents/backend-developer.md +167 -0
- package/src/stacks/backend-only/agents/backend-reviewer.md +89 -0
- package/src/stacks/backend-only/agents/orchestrator.md +69 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/SKILL.md +187 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/port.template.ts +62 -0
- package/src/stacks/backend-only/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
- package/src/stacks/backend-only/skills/mutation-testing/SKILL.md +129 -0
- package/src/stacks/mobile/agents/backend-developer.md +167 -0
- package/src/stacks/mobile/agents/backend-reviewer.md +89 -0
- package/src/stacks/mobile/agents/mobile-developer.md +70 -0
- package/src/stacks/mobile/agents/mobile-reviewer.md +175 -0
- package/src/stacks/mobile/agents/orchestrator.md +69 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/SKILL.md +187 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/port.template.ts +62 -0
- package/src/stacks/mobile/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
- package/src/stacks/mobile/skills/clean-hexa-mobile/SKILL.md +984 -0
- package/src/stacks/mobile/skills/mutation-testing/SKILL.md +129 -0
- package/src/stacks/web/agents/backend-developer.md +167 -0
- package/src/stacks/web/agents/backend-reviewer.md +89 -0
- package/src/stacks/web/agents/frontend-developer.md +65 -0
- package/src/stacks/web/agents/frontend-reviewer.md +92 -0
- package/src/stacks/web/agents/orchestrator.md +69 -0
- package/src/stacks/web/skills/clean-hexa-backend/SKILL.md +187 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/port.template.ts +62 -0
- package/src/stacks/web/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
- package/src/stacks/web/skills/clean-hexa-frontend/SKILL.md +172 -0
- package/src/stacks/web/skills/mutation-testing/SKILL.md +129 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Template: TypeORM Repository Adapter (Backend)
|
|
2
|
+
// Location: src/infrastructure/persistence/repositories/typeorm-[name].repository.ts
|
|
3
|
+
|
|
4
|
+
import { Injectable } from '@nestjs/common';
|
|
5
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
6
|
+
import { Repository } from 'typeorm';
|
|
7
|
+
// TODO: Import port interface from domain
|
|
8
|
+
// TODO: Import domain entity
|
|
9
|
+
// TODO: Import ORM entity
|
|
10
|
+
// TODO: Import persistence mapper
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* TypeORM Repository Adapter Template
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - Implements domain port interface
|
|
17
|
+
* - Uses TypeORM for persistence
|
|
18
|
+
* - Maps between ORM entities and domain entities
|
|
19
|
+
* - Handles database-specific concerns
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class TypeOrm[Name]Repository implements I[Name]Repository {
|
|
24
|
+
constructor(
|
|
25
|
+
@InjectRepository([Name]OrmEntity)
|
|
26
|
+
private readonly repository: Repository<[Name]OrmEntity>,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async findById(id: string): Promise<[DomainEntity] | null> {
|
|
30
|
+
const ormEntity = await this.repository.findOne({ where: { id } });
|
|
31
|
+
|
|
32
|
+
if (!ormEntity) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return [Name]PersistenceMapper.toDomain(ormEntity);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async findAll(options?: { skip?: number; take?: number }): Promise<[DomainEntity][]> {
|
|
40
|
+
const ormEntities = await this.repository.find({
|
|
41
|
+
skip: options?.skip,
|
|
42
|
+
take: options?.take,
|
|
43
|
+
order: { createdAt: 'DESC' },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return ormEntities.map([Name]PersistenceMapper.toDomain);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async findBy(criteria: Partial<[DomainEntity]>): Promise<[DomainEntity][]> {
|
|
50
|
+
const ormEntities = await this.repository.find({
|
|
51
|
+
where: this.mapCriteriaToOrm(criteria),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return ormEntities.map([Name]PersistenceMapper.toDomain);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async save(entity: [DomainEntity]): Promise<void> {
|
|
58
|
+
const ormEntity = [Name]PersistenceMapper.toOrm(entity);
|
|
59
|
+
await this.repository.save(ormEntity);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async delete(id: string): Promise<void> {
|
|
63
|
+
await this.repository.delete(id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async exists(id: string): Promise<boolean> {
|
|
67
|
+
const count = await this.repository.count({ where: { id } });
|
|
68
|
+
return count > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private mapCriteriaToOrm(criteria: Partial<[DomainEntity]>): Partial<[Name]OrmEntity> {
|
|
72
|
+
// TODO: Map domain criteria to ORM criteria
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Template: Controller (Backend)
|
|
2
|
+
// Location: src/presentation/controllers/[name].controller.ts
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Controller,
|
|
6
|
+
Get,
|
|
7
|
+
Post,
|
|
8
|
+
Put,
|
|
9
|
+
Delete,
|
|
10
|
+
Body,
|
|
11
|
+
Param,
|
|
12
|
+
Query,
|
|
13
|
+
HttpCode,
|
|
14
|
+
HttpStatus,
|
|
15
|
+
UseGuards,
|
|
16
|
+
ParseUUIDPipe,
|
|
17
|
+
} from '@nestjs/common';
|
|
18
|
+
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
|
19
|
+
// TODO: Import use cases
|
|
20
|
+
// TODO: Import DTOs
|
|
21
|
+
// TODO: Import guards if needed
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Controller Template
|
|
25
|
+
*
|
|
26
|
+
* Rules:
|
|
27
|
+
* - Thin controller (no business logic)
|
|
28
|
+
* - Delegate to use cases
|
|
29
|
+
* - Handle HTTP concerns (status codes, validation)
|
|
30
|
+
* - Document with Swagger decorators
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Request DTOs
|
|
34
|
+
export class Create[Name]RequestDto {
|
|
35
|
+
// TODO: Add properties with class-validator decorators
|
|
36
|
+
// @IsString()
|
|
37
|
+
// @IsNotEmpty()
|
|
38
|
+
// name: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Update[Name]RequestDto {
|
|
42
|
+
// TODO: Add properties
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Response DTOs
|
|
46
|
+
export class [Name]ResponseDto {
|
|
47
|
+
id: string;
|
|
48
|
+
// TODO: Add properties
|
|
49
|
+
createdAt: Date;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@ApiTags('[names]')
|
|
53
|
+
@Controller('[names]')
|
|
54
|
+
export class [Name]Controller {
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly create[Name]UseCase: Create[Name]UseCase,
|
|
57
|
+
private readonly get[Name]UseCase: Get[Name]UseCase,
|
|
58
|
+
private readonly update[Name]UseCase: Update[Name]UseCase,
|
|
59
|
+
private readonly delete[Name]UseCase: Delete[Name]UseCase,
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
@Post()
|
|
63
|
+
@HttpCode(HttpStatus.CREATED)
|
|
64
|
+
@ApiOperation({ summary: 'Create a new [name]' })
|
|
65
|
+
@ApiResponse({ status: 201, type: [Name]ResponseDto })
|
|
66
|
+
async create(@Body() dto: Create[Name]RequestDto): Promise<[Name]ResponseDto> {
|
|
67
|
+
const result = await this.create[Name]UseCase.execute({
|
|
68
|
+
// Map DTO to command
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return this.toResponseDto(result);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Get(':id')
|
|
75
|
+
@ApiOperation({ summary: 'Get [name] by ID' })
|
|
76
|
+
@ApiResponse({ status: 200, type: [Name]ResponseDto })
|
|
77
|
+
@ApiResponse({ status: 404, description: '[Name] not found' })
|
|
78
|
+
async findOne(
|
|
79
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
80
|
+
): Promise<[Name]ResponseDto> {
|
|
81
|
+
const result = await this.get[Name]UseCase.execute({ id });
|
|
82
|
+
|
|
83
|
+
if (!result) {
|
|
84
|
+
// Throw NotFoundException or let exception filter handle
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return this.toResponseDto(result);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Get()
|
|
91
|
+
@ApiOperation({ summary: 'List all [names]' })
|
|
92
|
+
@ApiResponse({ status: 200, type: [Name]ResponseDto, isArray: true })
|
|
93
|
+
async findAll(
|
|
94
|
+
@Query('skip') skip?: number,
|
|
95
|
+
@Query('take') take?: number,
|
|
96
|
+
): Promise<[Name]ResponseDto[]> {
|
|
97
|
+
const results = await this.list[Names]UseCase.execute({ skip, take });
|
|
98
|
+
return results.map(this.toResponseDto);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Put(':id')
|
|
102
|
+
@ApiOperation({ summary: 'Update [name]' })
|
|
103
|
+
@ApiResponse({ status: 200, type: [Name]ResponseDto })
|
|
104
|
+
async update(
|
|
105
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
106
|
+
@Body() dto: Update[Name]RequestDto,
|
|
107
|
+
): Promise<[Name]ResponseDto> {
|
|
108
|
+
const result = await this.update[Name]UseCase.execute({
|
|
109
|
+
id,
|
|
110
|
+
// Map DTO to command
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return this.toResponseDto(result);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Delete(':id')
|
|
117
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
118
|
+
@ApiOperation({ summary: 'Delete [name]' })
|
|
119
|
+
@ApiResponse({ status: 204 })
|
|
120
|
+
async delete(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
|
|
121
|
+
await this.delete[Name]UseCase.execute({ id });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private toResponseDto(entity: any): [Name]ResponseDto {
|
|
125
|
+
// TODO: Map entity to response DTO
|
|
126
|
+
return {
|
|
127
|
+
id: entity.id,
|
|
128
|
+
createdAt: entity.createdAt,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Template: Domain Entity (Backend)
|
|
2
|
+
// Location: src/domain/entities/[name].entity.ts
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Domain Entity Template
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - Immutable where possible
|
|
9
|
+
* - No NestJS/TypeORM imports
|
|
10
|
+
* - Private constructor + static factory methods
|
|
11
|
+
* - Business logic encapsulated
|
|
12
|
+
* - Separate from ORM entities
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// TODO: Import value objects
|
|
16
|
+
// TODO: Import domain errors
|
|
17
|
+
|
|
18
|
+
export class [EntityName] {
|
|
19
|
+
private constructor(
|
|
20
|
+
public readonly id: string,
|
|
21
|
+
public readonly createdAt: Date,
|
|
22
|
+
public readonly updatedAt: Date,
|
|
23
|
+
// TODO: Add entity properties
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Factory method for creating new entities
|
|
28
|
+
*/
|
|
29
|
+
static create(/* params */): [EntityName] {
|
|
30
|
+
// TODO: Add validation logic
|
|
31
|
+
const now = new Date();
|
|
32
|
+
return new [EntityName](
|
|
33
|
+
this.generateId(),
|
|
34
|
+
now,
|
|
35
|
+
now,
|
|
36
|
+
// ... properties
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Factory method for reconstituting from persistence
|
|
42
|
+
*/
|
|
43
|
+
static reconstitute(props: {
|
|
44
|
+
id: string;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
updatedAt: Date;
|
|
47
|
+
// TODO: Add properties
|
|
48
|
+
}): [EntityName] {
|
|
49
|
+
return new [EntityName](
|
|
50
|
+
props.id,
|
|
51
|
+
props.createdAt,
|
|
52
|
+
props.updatedAt,
|
|
53
|
+
// ... properties
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Business methods that return new instances (immutability)
|
|
59
|
+
*/
|
|
60
|
+
// TODO: Add domain behavior methods
|
|
61
|
+
// Example:
|
|
62
|
+
// confirm(): [EntityName] {
|
|
63
|
+
// if (this.status !== Status.PENDING) {
|
|
64
|
+
// throw new InvalidStateError('Can only confirm pending orders');
|
|
65
|
+
// }
|
|
66
|
+
// return new [EntityName](
|
|
67
|
+
// this.id,
|
|
68
|
+
// this.createdAt,
|
|
69
|
+
// new Date(),
|
|
70
|
+
// Status.CONFIRMED,
|
|
71
|
+
// );
|
|
72
|
+
// }
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Domain validation
|
|
76
|
+
*/
|
|
77
|
+
// private validate(): void {
|
|
78
|
+
// if (!this.someProperty) {
|
|
79
|
+
// throw new ValidationError('Property is required');
|
|
80
|
+
// }
|
|
81
|
+
// }
|
|
82
|
+
|
|
83
|
+
private static generateId(): string {
|
|
84
|
+
// Use UUID v4 or similar
|
|
85
|
+
return require('crypto').randomUUID();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Template: Port Interface (Backend)
|
|
2
|
+
// Location: src/domain/ports/repositories/[name].repository.port.ts
|
|
3
|
+
|
|
4
|
+
// TODO: Import entity from domain
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Port Interface Template
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* - Define contract, not implementation
|
|
11
|
+
* - Use domain types only (no TypeORM, no Prisma)
|
|
12
|
+
* - Export Symbol for NestJS DI
|
|
13
|
+
* - Async methods return Promise
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Injection token for NestJS DI
|
|
17
|
+
export const [NAME]_REPOSITORY = Symbol('[NAME]_REPOSITORY');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Repository port for [Name] aggregate
|
|
21
|
+
*/
|
|
22
|
+
export interface I[Name]Repository {
|
|
23
|
+
/**
|
|
24
|
+
* Find entity by ID
|
|
25
|
+
* @returns Promise resolving to entity or null if not found
|
|
26
|
+
*/
|
|
27
|
+
findById(id: string): Promise<[Entity] | null>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find all entities with optional pagination
|
|
31
|
+
*/
|
|
32
|
+
findAll(options?: {
|
|
33
|
+
skip?: number;
|
|
34
|
+
take?: number;
|
|
35
|
+
}): Promise<[Entity][]>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find entities matching criteria
|
|
39
|
+
*/
|
|
40
|
+
findBy(criteria: Partial<[Entity]>): Promise<[Entity][]>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Save entity (create or update)
|
|
44
|
+
*/
|
|
45
|
+
save(entity: [Entity]): Promise<void>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Delete entity by ID
|
|
49
|
+
*/
|
|
50
|
+
delete(id: string): Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if entity exists
|
|
54
|
+
*/
|
|
55
|
+
exists(id: string): Promise<boolean>;
|
|
56
|
+
|
|
57
|
+
// TODO: Add domain-specific query methods
|
|
58
|
+
// Example:
|
|
59
|
+
// findByStatus(status: EntityStatus): Promise<[Entity][]>;
|
|
60
|
+
// findByUserId(userId: string): Promise<[Entity][]>;
|
|
61
|
+
// countByStatus(status: EntityStatus): Promise<number>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Template: Use Case (Backend)
|
|
2
|
+
// Location: src/application/use-cases/[name]/[name].use-case.ts
|
|
3
|
+
|
|
4
|
+
import { Injectable, Inject } from '@nestjs/common';
|
|
5
|
+
// TODO: Import port symbol and interface from domain
|
|
6
|
+
// TODO: Import entity from domain
|
|
7
|
+
// TODO: Import domain errors
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Use Case Template
|
|
11
|
+
*
|
|
12
|
+
* Rules:
|
|
13
|
+
* - One class = One business action
|
|
14
|
+
* - Single execute() method
|
|
15
|
+
* - Inject ports via Symbol tokens
|
|
16
|
+
* - Return Result<T> or throw domain errors
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Command/Input DTO
|
|
20
|
+
export class [Name]Command {
|
|
21
|
+
constructor(
|
|
22
|
+
// TODO: Define command properties
|
|
23
|
+
public readonly userId: string,
|
|
24
|
+
) {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Result DTO
|
|
28
|
+
export interface [Name]Result {
|
|
29
|
+
// TODO: Define result properties
|
|
30
|
+
id: string;
|
|
31
|
+
success: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Injectable()
|
|
35
|
+
export class [Name]UseCase {
|
|
36
|
+
constructor(
|
|
37
|
+
@Inject([REPOSITORY]_REPOSITORY)
|
|
38
|
+
private readonly repository: I[Repository]Repository,
|
|
39
|
+
// TODO: Inject other ports if needed
|
|
40
|
+
// @Inject(PAYMENT_SERVICE)
|
|
41
|
+
// private readonly paymentService: IPaymentService,
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Execute the use case
|
|
46
|
+
* @throws DomainError on business rule violation
|
|
47
|
+
*/
|
|
48
|
+
async execute(command: [Name]Command): Promise<[Name]Result> {
|
|
49
|
+
// 1. Validate command
|
|
50
|
+
this.validateCommand(command);
|
|
51
|
+
|
|
52
|
+
// 2. Load required aggregates
|
|
53
|
+
// const entity = await this.repository.findById(command.entityId);
|
|
54
|
+
// if (!entity) {
|
|
55
|
+
// throw new EntityNotFoundError(command.entityId);
|
|
56
|
+
// }
|
|
57
|
+
|
|
58
|
+
// 3. Execute domain logic
|
|
59
|
+
// const updatedEntity = entity.doSomething();
|
|
60
|
+
|
|
61
|
+
// 4. Persist changes
|
|
62
|
+
// await this.repository.save(updatedEntity);
|
|
63
|
+
|
|
64
|
+
// 5. Return result
|
|
65
|
+
return {
|
|
66
|
+
id: 'generated-id',
|
|
67
|
+
success: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private validateCommand(command: [Name]Command): void {
|
|
72
|
+
// TODO: Add validation logic
|
|
73
|
+
// if (!command.userId) {
|
|
74
|
+
// throw new ValidationError('userId is required');
|
|
75
|
+
// }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mutation-testing
|
|
3
|
+
description: Mutation testing avec Stryker pour valider la qualite des tests.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill Mutation Testing
|
|
7
|
+
|
|
8
|
+
Le mutation testing valide que vos tests detectent vraiment les bugs.
|
|
9
|
+
|
|
10
|
+
## Principe
|
|
11
|
+
|
|
12
|
+
1. Stryker modifie (mute) votre code de production
|
|
13
|
+
2. Execute vos tests sur le code mute
|
|
14
|
+
3. Si les tests passent = mutant survit = test insuffisant
|
|
15
|
+
4. Si les tests echouent = mutant tue = test efficace
|
|
16
|
+
|
|
17
|
+
## Types de mutations
|
|
18
|
+
|
|
19
|
+
### Mutations arithmetiques
|
|
20
|
+
```typescript
|
|
21
|
+
// Original
|
|
22
|
+
const total = price * quantity;
|
|
23
|
+
|
|
24
|
+
// Mutant
|
|
25
|
+
const total = price / quantity; // * -> /
|
|
26
|
+
const total = price + quantity; // * -> +
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Mutations conditionnelles
|
|
30
|
+
```typescript
|
|
31
|
+
// Original
|
|
32
|
+
if (age >= 18)
|
|
33
|
+
|
|
34
|
+
// Mutants
|
|
35
|
+
if (age > 18) // >= -> >
|
|
36
|
+
if (age <= 18) // >= -> <=
|
|
37
|
+
if (true) // condition -> true
|
|
38
|
+
if (false) // condition -> false
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Mutations de blocs
|
|
42
|
+
```typescript
|
|
43
|
+
// Original
|
|
44
|
+
if (condition) {
|
|
45
|
+
doSomething();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Mutant
|
|
49
|
+
if (condition) {
|
|
50
|
+
// Block removed
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration Stryker
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
// stryker.conf.json
|
|
58
|
+
{
|
|
59
|
+
"mutate": [
|
|
60
|
+
"src/**/*.ts",
|
|
61
|
+
"!src/**/*.spec.ts",
|
|
62
|
+
"!src/**/*.module.ts"
|
|
63
|
+
],
|
|
64
|
+
"testRunner": "jest",
|
|
65
|
+
"reporters": ["html", "clear-text", "progress"],
|
|
66
|
+
"thresholds": {
|
|
67
|
+
"high": 80,
|
|
68
|
+
"low": 60,
|
|
69
|
+
"break": 75
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Interpretation du score
|
|
75
|
+
|
|
76
|
+
- **Score > 80%** : Excellent, tests robustes
|
|
77
|
+
- **Score 60-80%** : Acceptable, ameliorations possibles
|
|
78
|
+
- **Score < 60%** : Tests insuffisants
|
|
79
|
+
|
|
80
|
+
## Mutants survivants
|
|
81
|
+
|
|
82
|
+
Quand un mutant survit, analyser :
|
|
83
|
+
|
|
84
|
+
1. **Test manquant** : Ajouter un test pour ce cas
|
|
85
|
+
2. **Code mort** : Le code n'est pas necessaire
|
|
86
|
+
3. **Test trop general** : Affiner les assertions
|
|
87
|
+
|
|
88
|
+
### Exemple
|
|
89
|
+
```typescript
|
|
90
|
+
// Code
|
|
91
|
+
function calculateDiscount(price: number, isPremium: boolean): number {
|
|
92
|
+
if (isPremium) {
|
|
93
|
+
return price * 0.9; // 10% discount
|
|
94
|
+
}
|
|
95
|
+
return price;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Test insuffisant
|
|
99
|
+
it('should apply discount for premium', () => {
|
|
100
|
+
expect(calculateDiscount(100, true)).toBeLessThan(100);
|
|
101
|
+
// Mutant survit: price * 0.8 passe aussi!
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Test robuste
|
|
105
|
+
it('should apply 10% discount for premium', () => {
|
|
106
|
+
expect(calculateDiscount(100, true)).toBe(90);
|
|
107
|
+
// Mutant tue: seul 0.9 donne 90
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Commandes
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Lancer mutation testing complet
|
|
115
|
+
npm run stryker
|
|
116
|
+
|
|
117
|
+
# Sur fichiers specifiques
|
|
118
|
+
npx stryker run --mutate "src/application/use-cases/**/*.ts"
|
|
119
|
+
|
|
120
|
+
# Rapport HTML
|
|
121
|
+
open reports/mutation/html/index.html
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Integration workflow
|
|
125
|
+
|
|
126
|
+
Phase 4 (Review) inclut obligatoirement :
|
|
127
|
+
1. Score mutation >= 75%
|
|
128
|
+
2. Analyse des mutants survivants
|
|
129
|
+
3. Ajout de tests si necessaire
|