@polymorphism-tech/morph-spec 7.7.2 → 7.8.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 (42) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/CLAUDE.md +1 -1
  6. package/framework/MORPH.md +3 -2
  7. package/framework/agents.json +1 -1
  8. package/framework/evals/scenarios/dotnet-crud.yaml +1 -1
  9. package/framework/rules/dotnet-templates.md +0 -1
  10. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  11. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  12. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  13. package/framework/skills/core/morph-brainstorming/SKILL.md +2 -2
  14. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  15. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  16. package/framework/skills/core/morph-plan/SKILL.md +4 -3
  17. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  18. package/framework/skills/core/morph-review/SKILL.md +1 -1
  19. package/framework/skills/core/morph-stack-scan/SKILL.md +5 -5
  20. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  21. package/framework/standards/STANDARDS.json +1 -1
  22. package/framework/standards/architecture/vertical-slice/vertical-slice.md +34 -35
  23. package/framework/standards/backend/api/validation.md +2 -2
  24. package/framework/standards/backend/dotnet/scrutor.md +1 -3
  25. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +42 -37
  26. package/framework/standards/data/nosql/cache/redis.md +2 -2
  27. package/framework/templates/CLAUDE.md.core.template +1 -1
  28. package/framework/templates/README.md +0 -1
  29. package/framework/templates/REGISTRY.json +0 -21
  30. package/framework/templates/code/dotnet/abstractions/handler.cs +0 -3
  31. package/framework/templates/code/dotnet/constants/api-tags.cs +1 -1
  32. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +4 -5
  33. package/framework/templates/code/dotnet/feature/handler.cs.hbs +23 -24
  34. package/framework/templates/code/dotnet/jobs/job.cs +15 -15
  35. package/framework/templates/code/dotnet/test.cs +72 -101
  36. package/package.json +1 -1
  37. package/src/commands/project/doctor.js +4 -1
  38. package/src/commands/project/update.js +2 -0
  39. package/src/scripts/setup-infra.js +4 -1
  40. package/src/utils/banner.js +53 -37
  41. package/src/utils/file-copier.js +6 -1
  42. package/framework/templates/code/dotnet/backend/repository.cs +0 -141
@@ -6,7 +6,7 @@
6
6
 
7
7
  VSA + .NET 10 + EF Core + Neon (Postgres) + Microsoft Agent Framework + Next.js (App Router) + Tailwind + Framer Motion + Shadcn + Hangfire (quando necessário).
8
8
 
9
- Padrões proibidos por padrão: `Guid.NewGuid()` (use `Guid.CreateVersion7()`), classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, SQL cru sem justificativa. Acesso a dados é híbrido: `IRepository<T>` + `IUnitOfWork` para CRUD; `DbContext` direto no slice para queries específicas.
9
+ Padrões proibidos por padrão: `Guid.NewGuid()` (use `Guid.CreateVersion7()`), classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), SQL cru sem justificativa. Acesso a dados: `ApplicationDbContext` direto no slice (`DbSet<T>` já é repository, `DbContext` já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em `decisions.md`.
10
10
 
11
11
  ## Harness
12
12
 
@@ -30,7 +30,6 @@ framework/templates/
30
30
  │ ├── dotnet/
31
31
  │ │ ├── backend/
32
32
  │ │ │ ├── service.cs # Service layer
33
- │ │ │ ├── repository.cs # Data access layer
34
33
  │ │ │ └── test.cs # Unit tests
35
34
  │ │ ├── database/
36
35
  │ │ │ └── migration.cs # EF Core migrations
@@ -256,27 +256,6 @@
256
256
  "technology": null,
257
257
  "validatePlaceholders": true
258
258
  },
259
- {
260
- "id": "dotnet-backend-repository",
261
- "kind": "template",
262
- "name": ".NET Repository Class",
263
- "path": "code/dotnet/backend/repository.cs",
264
- "location": "framework",
265
- "category": "code",
266
- "phase": "implement",
267
- "required": false,
268
- "description": "C# repository class template with EF Core patterns",
269
- "outputName": "{FeatureName}Repository.cs",
270
- "placeholders": [],
271
- "stackSpecific": false,
272
- "engine": "handlebars",
273
- "deprecated": false,
274
- "technology": "dotnet",
275
- "applicableStacks": [
276
- "nextjs-neon"
277
- ],
278
- "validatePlaceholders": true
279
- },
280
259
  {
281
260
  "id": "dotnet-backend-test",
282
261
  "kind": "template",
@@ -1,6 +1,3 @@
1
- using Azure.Core;
2
-
3
-
4
1
  namespace {{pascalCase PROJECT_NAME}}.Abstractions
5
2
  {
6
3
  public interface IHandler<in TRequest, TResponse>
@@ -2,5 +2,5 @@ namespace {{pascalCase PROJECT_NAME}}.Constants;
2
2
 
3
3
  public static class ApiTags
4
4
  {
5
- public const string {{pascalCase FEATURE_NAME}} = "{{pascalCase FEATURE_NAME}}";
5
+ public const string {{pascalCase FEATURE_NAME}}s = "{{kebabCase FEATURE_NAME}}s";
6
6
  }
@@ -178,8 +178,8 @@ public static class {{pascalCase FEATURE_NAME}}Errors
178
178
  namespace {{NAMESPACE}}.Features.{{pascalCase FEATURE_NAME}}Feature.Create{{pascalCase FEATURE_NAME}};
179
179
 
180
180
  using {{NAMESPACE}}.Abstractions;
181
+ using {{NAMESPACE}}.Database;
181
182
  using {{NAMESPACE}}.Entities;
182
- using {{NAMESPACE}}.Repository;
183
183
 
184
184
  // TODO: adicionar todos os campos do request (campos obrigatórios para criação)
185
185
  public sealed record Create{{pascalCase FEATURE_NAME}}Request(/* string Name, decimal Price, ... */);
@@ -188,8 +188,7 @@ public sealed record Create{{pascalCase FEATURE_NAME}}Request(/* string Name, de
188
188
  public sealed record Create{{pascalCase FEATURE_NAME}}Response(Guid Id /* , string Name, ... */);
189
189
 
190
190
  public sealed class Create{{pascalCase FEATURE_NAME}}Handler(
191
- IRepository<{{pascalCase FEATURE_NAME}}> _repo,
192
- IUnitOfWork _unitOfWork) : IHandler<Create{{pascalCase FEATURE_NAME}}Request, Result<Create{{pascalCase FEATURE_NAME}}Response>>
191
+ ApplicationDbContext _db) : IHandler<Create{{pascalCase FEATURE_NAME}}Request, Result<Create{{pascalCase FEATURE_NAME}}Response>>
193
192
  {
194
193
  public async Task<Result<Create{{pascalCase FEATURE_NAME}}Response>> HandleAsync(
195
194
  Create{{pascalCase FEATURE_NAME}}Request command, CancellationToken cancellationToken)
@@ -202,8 +201,8 @@ public sealed class Create{{pascalCase FEATURE_NAME}}Handler(
202
201
  // Price = command.Price,
203
202
  };
204
203
 
205
- await _repo.AddAsync(entity, cancellationToken);
206
- await _unitOfWork.CommitAsync(cancellationToken);
204
+ _db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
205
+ await _db.SaveChangesAsync(cancellationToken);
207
206
 
208
207
  return Result.Success(new Create{{pascalCase FEATURE_NAME}}Response(entity.Id /* , entity.Name, ... */));
209
208
  }
@@ -1,24 +1,23 @@
1
- namespace {{pascalCase PROJECT_NAME}}.Features.{{pascalCase FEATURE_NAME}}Feature.{{pascalCase SLICE_NAME}};
2
-
3
- using {{pascalCase PROJECT_NAME}}.Abstractions;
4
- using {{pascalCase PROJECT_NAME}}.Entities;
5
- using {{pascalCase PROJECT_NAME}}.Repository;
6
-
7
- public sealed class {{pascalCase SLICE_NAME}}Handler(
8
- IRepository<{{pascalCase FEATURE_NAME}}> _repository,
9
- IUnitOfWork _unitOfWork) : IHandler<{{pascalCase SLICE_NAME}}Request, Result<{{pascalCase SLICE_NAME}}Response>>
10
- {
11
- public async Task<Result<{{pascalCase SLICE_NAME}}Response>> HandleAsync({{pascalCase SLICE_NAME}}Request command, CancellationToken cancellationToken)
12
- {
13
- var entity = new {{pascalCase FEATURE_NAME}}
14
- {
15
- Id = Guid.CreateVersion7(),
16
- // TODO: Map command properties to entity
17
- };
18
-
19
- await _repository.AddAsync(entity, cancellationToken);
20
- await _unitOfWork.CommitAsync(cancellationToken);
21
-
22
- return Result.Success(new {{pascalCase SLICE_NAME}}Response(entity.Id));
23
- }
24
- }
1
+ namespace {{pascalCase PROJECT_NAME}}.Features.{{pascalCase FEATURE_NAME}}Feature.{{pascalCase SLICE_NAME}};
2
+
3
+ using {{pascalCase PROJECT_NAME}}.Abstractions;
4
+ using {{pascalCase PROJECT_NAME}}.Database;
5
+ using {{pascalCase PROJECT_NAME}}.Entities;
6
+
7
+ public sealed class {{pascalCase SLICE_NAME}}Handler(
8
+ ApplicationDbContext _db) : IHandler<{{pascalCase SLICE_NAME}}Request, Result<{{pascalCase SLICE_NAME}}Response>>
9
+ {
10
+ public async Task<Result<{{pascalCase SLICE_NAME}}Response>> HandleAsync({{pascalCase SLICE_NAME}}Request command, CancellationToken cancellationToken)
11
+ {
12
+ var entity = new {{pascalCase FEATURE_NAME}}
13
+ {
14
+ Id = Guid.CreateVersion7(),
15
+ // TODO: Map command properties to entity
16
+ };
17
+
18
+ _db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
19
+ await _db.SaveChangesAsync(cancellationToken);
20
+
21
+ return Result.Success(new {{pascalCase SLICE_NAME}}Response(entity.Id));
22
+ }
23
+ }
@@ -1,22 +1,22 @@
1
1
  // ============================================================
2
- // HANGFIRE JOB TEMPLATE (VSA Pattern — Repository + UnitOfWork)
2
+ // HANGFIRE JOB TEMPLATE (VSA Pattern — ApplicationDbContext direto)
3
3
  // Generated by MORPH Framework
4
4
  // ============================================================
5
5
 
6
6
  using Hangfire;
7
+ using Microsoft.EntityFrameworkCore;
7
8
  using Microsoft.Extensions.Logging;
8
- using {{pascalCase PROJECT_NAME}}.Repository;
9
+ using {{pascalCase PROJECT_NAME}}.Database;
9
10
 
10
11
  namespace {{pascalCase PROJECT_NAME}}.Features.{{pascalCase FEATURE_NAME}}.Jobs;
11
12
 
12
13
  /// <summary>
13
14
  /// Background job for processing {{pascalCase FEATURE_NAME}}.
14
15
  /// Uses Hangfire for scheduling and retry handling.
15
- /// Injects IRepository + IUnitOfWork directly (no service layer).
16
+ /// Injects ApplicationDbContext directly (no service layer, no generic repository).
16
17
  /// </summary>
17
18
  public class {{pascalCase FEATURE_NAME}}ProcessorJob(
18
- IRepository<{{pascalCase FEATURE_NAME}}> repository,
19
- IUnitOfWork unitOfWork,
19
+ ApplicationDbContext db,
20
20
  I{{pascalCase FEATURE_NAME}}AnalyzerAgent analyzer,
21
21
  ILogger<{{pascalCase FEATURE_NAME}}ProcessorJob> logger) : I{{pascalCase FEATURE_NAME}}ProcessorJob
22
22
  {
@@ -34,8 +34,8 @@ public class {{pascalCase FEATURE_NAME}}ProcessorJob(
34
34
 
35
35
  try
36
36
  {
37
- // Get the entity directly from the repository
38
- var entity = await repository.GetByIdAsync(id, cancellationToken);
37
+ // Get the entity directly from the DbSet
38
+ var entity = await db.Set<{{pascalCase FEATURE_NAME}}>().FindAsync(new object[] { id }, cancellationToken);
39
39
  if (entity is null)
40
40
  {
41
41
  logger.LogWarning("{{pascalCase FEATURE_NAME}} with ID {Id} not found, skipping", id);
@@ -57,10 +57,9 @@ public class {{pascalCase FEATURE_NAME}}ProcessorJob(
57
57
  "{{pascalCase FEATURE_NAME}} {Id} analyzed. Confidence: {Confidence:P0}",
58
58
  id, analysis.ConfidenceScore);
59
59
 
60
- // Update entity directly via repository
60
+ // Update the tracked entity and persist
61
61
  entity.MarkAsCompleted(analysis);
62
- await repository.UpdateAsync(entity, cancellationToken);
63
- await unitOfWork.CommitAsync(cancellationToken);
62
+ await db.SaveChangesAsync(cancellationToken);
64
63
 
65
64
  logger.LogInformation("Completed {{pascalCase FEATURE_NAME}} processing for ID {Id}", id);
66
65
  }
@@ -104,10 +103,10 @@ public class {{pascalCase FEATURE_NAME}}ProcessorJob(
104
103
 
105
104
  /// <summary>
106
105
  /// Batch job for processing multiple {{pascalCase FEATURE_NAME}}s.
107
- /// Queries pending items via IRepository and enqueues individual jobs.
106
+ /// Queries pending items via LINQ on the DbSet and enqueues individual jobs.
108
107
  /// </summary>
109
108
  public class {{pascalCase FEATURE_NAME}}BatchProcessorJob(
110
- IRepository<{{pascalCase FEATURE_NAME}}> repository,
109
+ ApplicationDbContext db,
111
110
  ILogger<{{pascalCase FEATURE_NAME}}BatchProcessorJob> logger)
112
111
  {
113
112
  /// <summary>
@@ -120,8 +119,9 @@ public class {{pascalCase FEATURE_NAME}}BatchProcessorJob(
120
119
  {
121
120
  logger.LogInformation("Starting batch processing for pending {{pascalCase FEATURE_NAME}}s");
122
121
 
123
- var allItems = await repository.GetAllAsync(cancellationToken);
124
- var pending = allItems.Where(x => x.Status == {{pascalCase FEATURE_NAME}}Status.Pending).ToList();
122
+ var pending = await db.Set<{{pascalCase FEATURE_NAME}}>()
123
+ .Where(x => x.Status == {{pascalCase FEATURE_NAME}}Status.Pending)
124
+ .ToListAsync(cancellationToken);
125
125
 
126
126
  logger.LogInformation("Found {Count} pending {{pascalCase FEATURE_NAME}}s to process", pending.Count);
127
127
 
@@ -162,7 +162,7 @@ public class {{pascalCase FEATURE_NAME}}BatchProcessorJob(
162
162
  // options.WorkerCount = Environment.ProcessorCount * 2;
163
163
  // });
164
164
  //
165
- // // Register jobs (repository + unit of work already registered)
165
+ // // Register jobs (ApplicationDbContext already registered via AddDbContext)
166
166
  // builder.Services.AddScoped<I{{pascalCase FEATURE_NAME}}ProcessorJob, {{pascalCase FEATURE_NAME}}ProcessorJob>();
167
167
  // builder.Services.AddScoped<{{pascalCase FEATURE_NAME}}BatchProcessorJob>();
168
168
  //
@@ -6,9 +6,10 @@
6
6
  using FluentAssertions;
7
7
  using FluentValidation;
8
8
  using FluentValidation.Results;
9
- using Microsoft.Extensions.Logging;
10
- using NSubstitute;
11
- using {{pascalCase PROJECT_NAME}}.Repository;
9
+ using Microsoft.Data.Sqlite;
10
+ using Microsoft.EntityFrameworkCore;
11
+ using {{pascalCase PROJECT_NAME}}.Database;
12
+ using {{pascalCase PROJECT_NAME}}.Entities;
12
13
  using Xunit;
13
14
 
14
15
  namespace {{pascalCase PROJECT_NAME}}.Tests.Unit.Features.{{pascalCase FEATURE_NAME}};
@@ -19,19 +20,31 @@ namespace {{pascalCase PROJECT_NAME}}.Tests.Unit.Features.{{pascalCase FEATURE_N
19
20
 
20
21
  /// <summary>
21
22
  /// Unit tests for {{pascalCase FEATURE_NAME}} handlers.
22
- /// Tests handlers directly with mocked IRepository and IUnitOfWork.
23
+ /// Tests handlers directly against a real ApplicationDbContext backed by
24
+ /// in-memory SQLite (no mocks — DbSet is the repository, DbContext is the unit of work).
23
25
  /// </summary>
24
- public class {{pascalCase FEATURE_NAME}}HandlerTests
26
+ public class {{pascalCase FEATURE_NAME}}HandlerTests : IDisposable
25
27
  {
26
- private readonly IRepository<{{pascalCase FEATURE_NAME}}> _repository;
27
- private readonly IUnitOfWork _unitOfWork;
28
- private readonly ILogger<Create{{pascalCase FEATURE_NAME}}.Handler> _logger;
28
+ private readonly SqliteConnection _connection;
29
+ private readonly ApplicationDbContext _db;
29
30
 
30
31
  public {{pascalCase FEATURE_NAME}}HandlerTests()
31
32
  {
32
- _repository = Substitute.For<IRepository<{{pascalCase FEATURE_NAME}}>>();
33
- _unitOfWork = Substitute.For<IUnitOfWork>();
34
- _logger = Substitute.For<ILogger<Create{{pascalCase FEATURE_NAME}}.Handler>>();
33
+ _connection = new SqliteConnection("DataSource=:memory:");
34
+ _connection.Open();
35
+
36
+ var options = new DbContextOptionsBuilder<ApplicationDbContext>()
37
+ .UseSqlite(_connection)
38
+ .Options;
39
+
40
+ _db = new ApplicationDbContext(options);
41
+ _db.Database.EnsureCreated();
42
+ }
43
+
44
+ public void Dispose()
45
+ {
46
+ _db.Dispose();
47
+ _connection.Dispose();
35
48
  }
36
49
 
37
50
  #region Create Handler Tests
@@ -40,14 +53,8 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
40
53
  public async Task Create_HandleAsync_WithValidCommand_ReturnsSuccessResult()
41
54
  {
42
55
  // Arrange
43
- var command = new Create{{pascalCase FEATURE_NAME}}.Command("Test {{pascalCase FEATURE_NAME}}");
44
- var handler = new Create{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork, _logger);
45
-
46
- _repository.AddAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>())
47
- .Returns(callInfo => callInfo.Arg<{{pascalCase FEATURE_NAME}}>());
48
-
49
- _unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
50
- .Returns(1);
56
+ var command = new Create{{pascalCase FEATURE_NAME}}Request("Test {{pascalCase FEATURE_NAME}}");
57
+ var handler = new Create{{pascalCase FEATURE_NAME}}Handler(_db);
51
58
 
52
59
  // Act
53
60
  var result = await handler.HandleAsync(command, CancellationToken.None);
@@ -55,31 +62,9 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
55
62
  // Assert
56
63
  result.IsSuccess.Should().BeTrue();
57
64
  result.Value.Should().NotBeNull();
58
- result.Value.Name.Should().Be("Test {{pascalCase FEATURE_NAME}}");
59
65
 
60
- await _repository.Received(1).AddAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>());
61
- await _unitOfWork.Received(1).CommitAsync(Arg.Any<CancellationToken>());
62
- }
63
-
64
- [Fact]
65
- public async Task Create_HandleAsync_WhenCommitFails_ReturnsFailureResult()
66
- {
67
- // Arrange
68
- var command = new Create{{pascalCase FEATURE_NAME}}.Command("Test {{pascalCase FEATURE_NAME}}");
69
- var handler = new Create{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork, _logger);
70
-
71
- _repository.AddAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>())
72
- .Returns(callInfo => callInfo.Arg<{{pascalCase FEATURE_NAME}}>());
73
-
74
- _unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
75
- .ThrowsAsync(new InvalidOperationException("Database error"));
76
-
77
- // Act
78
- var act = async () => await handler.HandleAsync(command, CancellationToken.None);
79
-
80
- // Assert
81
- await act.Should().ThrowAsync<InvalidOperationException>()
82
- .WithMessage("Database error");
66
+ var persisted = await _db.Set<{{pascalCase FEATURE_NAME}}>().SingleAsync();
67
+ persisted.Name.Should().Be("Test {{pascalCase FEATURE_NAME}}");
83
68
  }
84
69
 
85
70
  #endregion
@@ -90,12 +75,12 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
90
75
  public async Task GetById_HandleAsync_WithValidId_ReturnsSuccessResult()
91
76
  {
92
77
  // Arrange
93
- var query = new GetById{{pascalCase FEATURE_NAME}}.Query(1);
94
- var entity = CreateTestEntity(1, "Test {{pascalCase FEATURE_NAME}}");
95
- var handler = new GetById{{pascalCase FEATURE_NAME}}.Handler(_repository);
78
+ var entity = CreateTestEntity("Test {{pascalCase FEATURE_NAME}}");
79
+ _db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
80
+ await _db.SaveChangesAsync();
96
81
 
97
- _repository.GetByIdAsync(1, Arg.Any<CancellationToken>())
98
- .Returns(entity);
82
+ var query = new Get{{pascalCase FEATURE_NAME}}ByIdRequest(entity.Id);
83
+ var handler = new Get{{pascalCase FEATURE_NAME}}ByIdHandler(_db);
99
84
 
100
85
  // Act
101
86
  var result = await handler.HandleAsync(query, CancellationToken.None);
@@ -110,11 +95,8 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
110
95
  public async Task GetById_HandleAsync_WithInvalidId_ReturnsFailureResult()
111
96
  {
112
97
  // Arrange
113
- var query = new GetById{{pascalCase FEATURE_NAME}}.Query(999);
114
- var handler = new GetById{{pascalCase FEATURE_NAME}}.Handler(_repository);
115
-
116
- _repository.GetByIdAsync(999, Arg.Any<CancellationToken>())
117
- .Returns(({{pascalCase FEATURE_NAME}}?)null);
98
+ var query = new Get{{pascalCase FEATURE_NAME}}ByIdRequest(Guid.CreateVersion7());
99
+ var handler = new Get{{pascalCase FEATURE_NAME}}ByIdHandler(_db);
118
100
 
119
101
  // Act
120
102
  var result = await handler.HandleAsync(query, CancellationToken.None);
@@ -131,15 +113,12 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
131
113
  public async Task Update_HandleAsync_WithValidCommand_ReturnsSuccessResult()
132
114
  {
133
115
  // Arrange
134
- var command = new Update{{pascalCase FEATURE_NAME}}.Command(1, "Updated Name");
135
- var entity = CreateTestEntity(1, "Original Name");
136
- var handler = new Update{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork);
137
-
138
- _repository.GetByIdAsync(1, Arg.Any<CancellationToken>())
139
- .Returns(entity);
116
+ var entity = CreateTestEntity("Original Name");
117
+ _db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
118
+ await _db.SaveChangesAsync();
140
119
 
141
- _unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
142
- .Returns(1);
120
+ var command = new Update{{pascalCase FEATURE_NAME}}Request(entity.Id, "Updated Name");
121
+ var handler = new Update{{pascalCase FEATURE_NAME}}Handler(_db);
143
122
 
144
123
  // Act
145
124
  var result = await handler.HandleAsync(command, CancellationToken.None);
@@ -147,26 +126,22 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
147
126
  // Assert
148
127
  result.IsSuccess.Should().BeTrue();
149
128
 
150
- await _repository.Received(1).UpdateAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>());
151
- await _unitOfWork.Received(1).CommitAsync(Arg.Any<CancellationToken>());
129
+ var persisted = await _db.Set<{{pascalCase FEATURE_NAME}}>().SingleAsync(x => x.Id == entity.Id);
130
+ persisted.Name.Should().Be("Updated Name");
152
131
  }
153
132
 
154
133
  [Fact]
155
134
  public async Task Update_HandleAsync_WithNonExistentId_ReturnsFailureResult()
156
135
  {
157
136
  // Arrange
158
- var command = new Update{{pascalCase FEATURE_NAME}}.Command(999, "Updated");
159
- var handler = new Update{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork);
160
-
161
- _repository.GetByIdAsync(999, Arg.Any<CancellationToken>())
162
- .Returns(({{pascalCase FEATURE_NAME}}?)null);
137
+ var command = new Update{{pascalCase FEATURE_NAME}}Request(Guid.CreateVersion7(), "Updated");
138
+ var handler = new Update{{pascalCase FEATURE_NAME}}Handler(_db);
163
139
 
164
140
  // Act
165
141
  var result = await handler.HandleAsync(command, CancellationToken.None);
166
142
 
167
143
  // Assert
168
144
  result.IsSuccess.Should().BeFalse();
169
- await _unitOfWork.DidNotReceive().CommitAsync(Arg.Any<CancellationToken>());
170
145
  }
171
146
 
172
147
  #endregion
@@ -177,15 +152,12 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
177
152
  public async Task Delete_HandleAsync_WithValidId_ReturnsSuccessResult()
178
153
  {
179
154
  // Arrange
180
- var command = new Delete{{pascalCase FEATURE_NAME}}.Command(1);
181
- var entity = CreateTestEntity(1, "To Delete");
182
- var handler = new Delete{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork);
155
+ var entity = CreateTestEntity("To Delete");
156
+ _db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
157
+ await _db.SaveChangesAsync();
183
158
 
184
- _repository.GetByIdAsync(1, Arg.Any<CancellationToken>())
185
- .Returns(entity);
186
-
187
- _unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
188
- .Returns(1);
159
+ var command = new Delete{{pascalCase FEATURE_NAME}}Request(entity.Id);
160
+ var handler = new Delete{{pascalCase FEATURE_NAME}}Handler(_db);
189
161
 
190
162
  // Act
191
163
  var result = await handler.HandleAsync(command, CancellationToken.None);
@@ -193,36 +165,35 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
193
165
  // Assert
194
166
  result.IsSuccess.Should().BeTrue();
195
167
 
196
- await _repository.Received(1).DeleteAsync(entity, Arg.Any<CancellationToken>());
197
- await _unitOfWork.Received(1).CommitAsync(Arg.Any<CancellationToken>());
168
+ (await _db.Set<{{pascalCase FEATURE_NAME}}>().AnyAsync(x => x.Id == entity.Id)).Should().BeFalse();
198
169
  }
199
170
 
200
171
  [Fact]
201
172
  public async Task Delete_HandleAsync_WithNonExistentId_ReturnsFailureResult()
202
173
  {
203
174
  // Arrange
204
- var command = new Delete{{pascalCase FEATURE_NAME}}.Command(999);
205
- var handler = new Delete{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork);
206
-
207
- _repository.GetByIdAsync(999, Arg.Any<CancellationToken>())
208
- .Returns(({{pascalCase FEATURE_NAME}}?)null);
175
+ var command = new Delete{{pascalCase FEATURE_NAME}}Request(Guid.CreateVersion7());
176
+ var handler = new Delete{{pascalCase FEATURE_NAME}}Handler(_db);
209
177
 
210
178
  // Act
211
179
  var result = await handler.HandleAsync(command, CancellationToken.None);
212
180
 
213
181
  // Assert
214
182
  result.IsSuccess.Should().BeFalse();
215
- await _unitOfWork.DidNotReceive().CommitAsync(Arg.Any<CancellationToken>());
216
183
  }
217
184
 
218
185
  #endregion
219
186
 
220
187
  #region Helper Methods
221
188
 
222
- private static {{pascalCase FEATURE_NAME}} CreateTestEntity(int id, string name)
189
+ private static {{pascalCase FEATURE_NAME}} CreateTestEntity(string name)
223
190
  {
224
- return {{pascalCase FEATURE_NAME}}.Create(name);
225
- // Note: In real tests, use reflection or a test factory to set the Id
191
+ return new {{pascalCase FEATURE_NAME}}
192
+ {
193
+ Id = Guid.CreateVersion7(),
194
+ Name = name,
195
+ // TODO: preencher demais campos obrigatórios da entidade
196
+ };
226
197
  }
227
198
 
228
199
  #endregion
@@ -244,8 +215,8 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
244
215
  public async Task CreateValidator_WithValidCommand_PassesValidation()
245
216
  {
246
217
  // Arrange
247
- var validator = new Create{{pascalCase FEATURE_NAME}}.Validator();
248
- var command = new Create{{pascalCase FEATURE_NAME}}.Command("Valid Name");
218
+ var validator = new Create{{pascalCase FEATURE_NAME}}Validator();
219
+ var command = new Create{{pascalCase FEATURE_NAME}}Request("Valid Name");
249
220
 
250
221
  // Act
251
222
  ValidationResult result = await validator.ValidateAsync(command);
@@ -262,30 +233,30 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
262
233
  public async Task CreateValidator_WithInvalidName_FailsValidation(string? name)
263
234
  {
264
235
  // Arrange
265
- var validator = new Create{{pascalCase FEATURE_NAME}}.Validator();
266
- var command = new Create{{pascalCase FEATURE_NAME}}.Command(name!);
236
+ var validator = new Create{{pascalCase FEATURE_NAME}}Validator();
237
+ var command = new Create{{pascalCase FEATURE_NAME}}Request(name!);
267
238
 
268
239
  // Act
269
240
  ValidationResult result = await validator.ValidateAsync(command);
270
241
 
271
242
  // Assert
272
243
  result.IsValid.Should().BeFalse();
273
- result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}.Command.Name));
244
+ result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}Request.Name));
274
245
  }
275
246
 
276
247
  [Fact]
277
248
  public async Task CreateValidator_WithNameExceedingMaxLength_FailsValidation()
278
249
  {
279
250
  // Arrange
280
- var validator = new Create{{pascalCase FEATURE_NAME}}.Validator();
281
- var command = new Create{{pascalCase FEATURE_NAME}}.Command(new string('A', 201));
251
+ var validator = new Create{{pascalCase FEATURE_NAME}}Validator();
252
+ var command = new Create{{pascalCase FEATURE_NAME}}Request(new string('A', 201));
282
253
 
283
254
  // Act
284
255
  ValidationResult result = await validator.ValidateAsync(command);
285
256
 
286
257
  // Assert
287
258
  result.IsValid.Should().BeFalse();
288
- result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}.Command.Name));
259
+ result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}Request.Name));
289
260
  }
290
261
 
291
262
  #endregion
@@ -296,8 +267,8 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
296
267
  public async Task UpdateValidator_WithValidCommand_PassesValidation()
297
268
  {
298
269
  // Arrange
299
- var validator = new Update{{pascalCase FEATURE_NAME}}.Validator();
300
- var command = new Update{{pascalCase FEATURE_NAME}}.Command(1, "Updated Name");
270
+ var validator = new Update{{pascalCase FEATURE_NAME}}Validator();
271
+ var command = new Update{{pascalCase FEATURE_NAME}}Request(Guid.CreateVersion7(), "Updated Name");
301
272
 
302
273
  // Act
303
274
  ValidationResult result = await validator.ValidateAsync(command);
@@ -310,15 +281,15 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
310
281
  public async Task UpdateValidator_WithInvalidId_FailsValidation()
311
282
  {
312
283
  // Arrange
313
- var validator = new Update{{pascalCase FEATURE_NAME}}.Validator();
314
- var command = new Update{{pascalCase FEATURE_NAME}}.Command(0, "Updated Name");
284
+ var validator = new Update{{pascalCase FEATURE_NAME}}Validator();
285
+ var command = new Update{{pascalCase FEATURE_NAME}}Request(Guid.Empty, "Updated Name");
315
286
 
316
287
  // Act
317
288
  ValidationResult result = await validator.ValidateAsync(command);
318
289
 
319
290
  // Assert
320
291
  result.IsValid.Should().BeFalse();
321
- result.Errors.Should().Contain(e => e.PropertyName == nameof(Update{{pascalCase FEATURE_NAME}}.Command.Id));
292
+ result.Errors.Should().Contain(e => e.PropertyName == nameof(Update{{pascalCase FEATURE_NAME}}Request.Id));
322
293
  }
323
294
 
324
295
  #endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "7.7.2",
3
+ "version": "7.8.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -196,7 +196,10 @@ async function doctorFullCommand(frameworkRoot) {
196
196
  let projectTags = [];
197
197
  const cfgPath = join(frameworkRoot, '.morph/config/config.json');
198
198
  if (await pathExists(cfgPath)) {
199
- try { projectTags = getProjectTags(JSON.parse(await fs.readFile(cfgPath, 'utf8'))); } catch { /* no tags */ }
199
+ try {
200
+ const rawCfg = (await fs.readFile(cfgPath, 'utf8')).replace(/^/, '');
201
+ projectTags = getProjectTags(JSON.parse(rawCfg));
202
+ } catch { /* no tags */ }
200
203
  }
201
204
  const drift = checkAgentsDrift(frameworkRoot, join(frameworkRoot, '.morph/framework'), projectTags);
202
205
  check(`Personas installed (${drift.expected - drift.missing.length}/${drift.expected})`,
@@ -410,6 +410,8 @@ export async function updateCommand(options) {
410
410
  if (updateStandards) logger.dim(' ✓ .morph/framework/standards/');
411
411
  logger.dim(' ✓ .morph/framework/hooks/');
412
412
  logger.dim(' ✓ .morph/framework/agents.json');
413
+ logger.dim(' ✓ .morph/framework/MORPH.md + phases.json (NLH charter)');
414
+ logger.dim(' ✓ .morph/framework/schemas/ + rubrics/ + evals/ (bar 9 + LLM rubrics)');
413
415
  if (commandsCopied) logger.dim(' ✓ .claude/commands/');
414
416
 
415
417
  logger.dim(` ✓ .claude/settings.local.json (${hooksResult.installed} hooks installed)`);
@@ -92,7 +92,10 @@ async function installStackAwareAssets(targetPath, log) {
92
92
  let projectTags = [];
93
93
  if (await pathExists(configPath)) {
94
94
  try {
95
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
95
+ // BOM-tolerant: a config.json hand-edited on Windows (PS 5.1 Out-File)
96
+ // carries a UTF-8 BOM; without stripping it this parse fails silently
97
+ // and only universal personas get installed.
98
+ const config = JSON.parse(readFileSync(configPath, 'utf-8').replace(/^/, ''));
96
99
  projectTags = getProjectTags(config);
97
100
  } catch { /* ignore malformed config */ }
98
101
  }