@drunkcoding/dknet-implementation-skills 0.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/.claude-plugin/marketplace.json +22 -0
- package/.claude-plugin/plugin.json +38 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/agents/dknet-architect.md +49 -0
- package/agents/dknet-bdd-engineer.md +62 -0
- package/agents/dknet-implementer.md +75 -0
- package/package.json +52 -0
- package/plugin.json +26 -0
- package/skills/README.md +47 -0
- package/skills/dknet-auth-and-ownership/SKILL.md +418 -0
- package/skills/dknet-bdd-tests/SKILL.md +355 -0
- package/skills/dknet-bdd-tests/checklist.md +39 -0
- package/skills/dknet-crud/SKILL.md +483 -0
- package/skills/dknet-ddd-principles/SKILL.md +87 -0
- package/skills/dknet-docs/SKILL.md +296 -0
- package/skills/dknet-docs/checklist.md +58 -0
- package/skills/dknet-docs/templates/README-template.md +68 -0
- package/skills/dknet-docs/templates/api-reference-template.md +275 -0
- package/skills/dknet-docs/templates/architecture-template.md +166 -0
- package/skills/dknet-docs/templates/data-model-template.md +99 -0
- package/skills/dknet-docs/templates/events-template.md +155 -0
- package/skills/dknet-dto-mapping/SKILL.md +278 -0
- package/skills/dknet-efcore-config/SKILL.md +379 -0
- package/skills/dknet-endpoint/SKILL.md +458 -0
- package/skills/dknet-entity/SKILL.md +483 -0
- package/skills/dknet-feature/SKILL.md +139 -0
- package/skills/dknet-feature-lifecycle/SKILL.md +144 -0
- package/skills/dknet-feature-remove/SKILL.md +131 -0
- package/skills/dknet-messaging-events/SKILL.md +395 -0
- package/skills/dknet-package-adoption/SKILL.md +252 -0
- package/skills/dknet-platform-config/SKILL.md +342 -0
- package/skills/dknet-project-structure/SKILL.md +148 -0
- package/skills/dknet-queries-specs/SKILL.md +330 -0
- package/skills/dknet-scaffold/SKILL.md +209 -0
- package/skills/dknet-unit-tests/SKILL.md +382 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dknet-entity
|
|
3
|
+
description: Create DDD domain entities following this project's AggregateRoot/DomainEntity inheritance pattern, either hand-written (mode=manual) or generator-declared (mode=auto). Use when adding a new domain entity or owned type to Minimal.Domains. Invoke as `/dknet-entity <Feature> <Entity> [mode=manual|auto] [props…]` to scaffold it for a feature.
|
|
4
|
+
metadata:
|
|
5
|
+
kind: workflow
|
|
6
|
+
arguments: "<Feature> <Entity> [mode=manual|auto] [props…] e.g. Orders Order mode=manual Number:string Total:decimal"
|
|
7
|
+
allowed-tools: Read, Grep, Glob, Edit, Write, Bash, Agent
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Usage: `/dknet-entity <Feature> <Entity> [mode=manual|auto] [props…] e.g. Orders Order mode=manual Number:string Total:decimal`
|
|
11
|
+
|
|
12
|
+
# Skill: Domain Entity Definition
|
|
13
|
+
|
|
14
|
+
Create domain entities that integrate with this project's DDD infrastructure — `AggregateRoot`,
|
|
15
|
+
`DomainEntity`, and (rarely) owned value objects — in either of the two shapes this template ships:
|
|
16
|
+
hand-written (`mode=manual`, mirrors `ManualSample/PurchaseOrder`) or generator-declared
|
|
17
|
+
(`mode=auto`, mirrors `AutomatedSample/Product`).
|
|
18
|
+
|
|
19
|
+
If the aggregate boundary, entity-vs-value-object choice, or invariant placement isn't obvious, read
|
|
20
|
+
`dknet-ddd-principles` first — this skill covers class mechanics, not those judgment calls. Event
|
|
21
|
+
**consumers** (handlers that react to a raised event) are covered by `dknet-messaging-events`, not
|
|
22
|
+
here — this skill only covers how an entity raises an event.
|
|
23
|
+
|
|
24
|
+
## Hierarchy
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
AuditedEntity<Guid> (DKNet.EfCore.Abstractions.Entities — Id, CreatedBy/On, UpdatedBy/On)
|
|
28
|
+
└── DomainEntity (Minimal.Domains/Share/DomainEntity.cs)
|
|
29
|
+
└── AggregateRoot (Minimal.Domains/Share/AggregateRoot.cs)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`Minimal.Domains/Share/DomainEntity.cs`:
|
|
33
|
+
|
|
34
|
+
```csharp
|
|
35
|
+
public abstract class DomainEntity : AuditedEntity<Guid>
|
|
36
|
+
{
|
|
37
|
+
protected DomainEntity(Guid id, string createdBy, DateTimeOffset? createdOn = null) : base(id)
|
|
38
|
+
{
|
|
39
|
+
SetCreatedBy(createdBy, createdOn);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected DomainEntity()
|
|
43
|
+
{
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`Minimal.Domains/Share/AggregateRoot.cs`:
|
|
49
|
+
|
|
50
|
+
```csharp
|
|
51
|
+
public abstract class AggregateRoot : DomainEntity
|
|
52
|
+
{
|
|
53
|
+
protected AggregateRoot(string createdBy, DateTimeOffset? createdOn = null)
|
|
54
|
+
: this(Guid.NewGuid(), createdBy, createdOn)
|
|
55
|
+
{
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected AggregateRoot(Guid id, string createdBy, DateTimeOffset? createdOn = null)
|
|
59
|
+
: base(id, createdBy, createdOn)
|
|
60
|
+
{
|
|
61
|
+
SetCreatedBy(createdBy, createdOn);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected AggregateRoot()
|
|
65
|
+
{
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`AuditedEntity<Guid>` (`DKNet.EfCore.Abstractions`) supplies `Id`, `CreatedBy`, `CreatedOn`,
|
|
71
|
+
`UpdatedBy`, `UpdatedOn`, `LastModifiedBy`/`LastModifiedOn` (computed: `UpdatedBy ?? CreatedBy`, not
|
|
72
|
+
mapped columns), plus `protected SetCreatedBy(userName, createdOn?)` (no-op once `CreatedBy` is
|
|
73
|
+
already set) and `protected SetUpdatedBy(userName, updatedOn?)`. All four base classes are abstract
|
|
74
|
+
and every property setter is `private` — don't redeclare any of them.
|
|
75
|
+
|
|
76
|
+
**Two ways an aggregate ends up with an `Id`:**
|
|
77
|
+
|
|
78
|
+
- Call the `AggregateRoot(string createdBy)` overload (or `(Guid id, string createdBy)`) — this
|
|
79
|
+
eagerly assigns `Id = Guid.NewGuid()` (or the given `id`) and stamps `CreatedBy` in memory, before
|
|
80
|
+
`SaveChanges`. `PurchaseOrder`'s public constructor does this via `: base(byUser)`.
|
|
81
|
+
- Call neither — the parameterless `protected AggregateRoot()` runs, `Id` stays `Guid.Empty` and
|
|
82
|
+
`CreatedBy` stays unset until `SaveChanges`. EF Core's mapper base class
|
|
83
|
+
(`DefaultEntityTypeConfiguration<T>`) configures `Id` with `ValueGeneratedOnAdd().HasValueGenerator<GuidV7ValueGenerator>()`,
|
|
84
|
+
so a fresh time-ordered GUID is assigned there instead, and the audit/data-ownership hooks stamp
|
|
85
|
+
`CreatedBy`/`OwnedBy` from the authenticated caller in the same save (see `dknet-efcore-config` and
|
|
86
|
+
the auditing rules below). `Product`'s `[CrudCreate]` constructor does this — it deliberately never
|
|
87
|
+
calls `base(byUser)`, because that overload requires an acting-user string the generated request
|
|
88
|
+
must not carry (see Acting user, below).
|
|
89
|
+
|
|
90
|
+
## mode=manual — hand-written (`ManualSample/PurchaseOrder`)
|
|
91
|
+
|
|
92
|
+
`Minimal.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`:
|
|
93
|
+
|
|
94
|
+
```csharp
|
|
95
|
+
public enum PurchaseOrderStatus { Draft, Placed, Cancelled }
|
|
96
|
+
|
|
97
|
+
public sealed class PurchaseOrder : AggregateRoot
|
|
98
|
+
{
|
|
99
|
+
public PurchaseOrder(string customerName, decimal amount, string byUser)
|
|
100
|
+
: base(byUser)
|
|
101
|
+
{
|
|
102
|
+
CustomerName = customerName;
|
|
103
|
+
Amount = amount;
|
|
104
|
+
Status = PurchaseOrderStatus.Placed;
|
|
105
|
+
|
|
106
|
+
AddEvent(new PurchaseOrderCreatedEvent(Id, CustomerName, Amount));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Rehydrates with a known identity (static seeding only); does not re-raise PurchaseOrderCreatedEvent.
|
|
110
|
+
internal PurchaseOrder(Guid id, string customerName, decimal amount, string byUser)
|
|
111
|
+
: base(id, byUser)
|
|
112
|
+
{
|
|
113
|
+
CustomerName = customerName;
|
|
114
|
+
Amount = amount;
|
|
115
|
+
Status = PurchaseOrderStatus.Placed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private PurchaseOrder()
|
|
119
|
+
{
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public string CustomerName { get; private set; } = null!;
|
|
123
|
+
public decimal Amount { get; private set; }
|
|
124
|
+
public PurchaseOrderStatus Status { get; private set; }
|
|
125
|
+
|
|
126
|
+
public void ChangeAmount(decimal amount, string userId)
|
|
127
|
+
{
|
|
128
|
+
Amount = amount;
|
|
129
|
+
SetUpdatedBy(userId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public void Cancel(string userId)
|
|
133
|
+
{
|
|
134
|
+
Status = PurchaseOrderStatus.Cancelled;
|
|
135
|
+
SetUpdatedBy(userId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`Minimal.Domains/Features/ManualSample/Entities/PurchaseOrderCreatedEvent.cs` — the whole file:
|
|
141
|
+
|
|
142
|
+
```csharp
|
|
143
|
+
public sealed record PurchaseOrderCreatedEvent(Guid Id, string CustomerName, decimal Amount);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Rules this shape follows:
|
|
147
|
+
|
|
148
|
+
- **Three constructors.** A public one for a new entity (forwards to `base(byUser)`), an `internal`
|
|
149
|
+
rehydration one taking `Guid id` first (forwards to `base(id, byUser)`, does **not** re-raise the
|
|
150
|
+
created event), and a `private` parameterless one for EF Core materialization.
|
|
151
|
+
- `sealed` — `PurchaseOrder` is `sealed`, and sealed is the shape to default to; nothing about a
|
|
152
|
+
hand-written entity needs it to be open for inheritance.
|
|
153
|
+
- Every property is `{ get; private set; }`. Mutation happens only through named methods
|
|
154
|
+
(`ChangeAmount`, `Cancel`) — never a single generic `Update(...)`, and never a public setter.
|
|
155
|
+
- Every mutation method calls `SetUpdatedBy(userId)` at the end.
|
|
156
|
+
- The domain event is raised by hand: `AddEvent(new PurchaseOrderCreatedEvent(...))` inside the
|
|
157
|
+
constructor, right where the aggregate becomes valid. The event itself is a plain
|
|
158
|
+
`public sealed record` next to the entity, in the same file or the same folder.
|
|
159
|
+
|
|
160
|
+
## mode=auto — generator-declared (`AutomatedSample/Product`)
|
|
161
|
+
|
|
162
|
+
`Minimal.Domains/Features/AutomatedSample/Entities/Product.cs` (XML docs trimmed):
|
|
163
|
+
|
|
164
|
+
```csharp
|
|
165
|
+
[RaisesEvent(EventOperations.Created, Include = [nameof(Id), nameof(Name), nameof(Price)])]
|
|
166
|
+
[RaisesEvent(EventOperations.Updated, nameof(Price))]
|
|
167
|
+
[RaisesEvent(EventOperations.Updated, nameof(IsDiscontinued))]
|
|
168
|
+
public class Product : AggregateRoot, IOwnedBy
|
|
169
|
+
{
|
|
170
|
+
[CrudCreate]
|
|
171
|
+
public Product(
|
|
172
|
+
[Required, StringLength(150)] string name,
|
|
173
|
+
[Range(0.01, double.MaxValue)] decimal price,
|
|
174
|
+
decimal? supplierCostPrice = null)
|
|
175
|
+
{
|
|
176
|
+
Name = name;
|
|
177
|
+
Price = price;
|
|
178
|
+
SupplierCostPrice = supplierCostPrice;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
protected Product()
|
|
182
|
+
{
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public string Name { get; private set; } = null!;
|
|
186
|
+
public decimal Price { get; private set; }
|
|
187
|
+
public bool IsDiscontinued { get; private set; }
|
|
188
|
+
|
|
189
|
+
// Stamped by DataOwnerHook; makes the row subject to the global read filter.
|
|
190
|
+
public string OwnedBy { get; private set; } = string.Empty;
|
|
191
|
+
|
|
192
|
+
[SensitiveData("pricing")] public decimal? SupplierCostPrice { get; private set; }
|
|
193
|
+
[SensitiveData] public string? SupplierReferenceCode { get; private set; }
|
|
194
|
+
|
|
195
|
+
[CrudUpdate]
|
|
196
|
+
public void ChangePrice([Range(0.01, double.MaxValue)] decimal price) => Price = price;
|
|
197
|
+
|
|
198
|
+
[CrudAction("approval")]
|
|
199
|
+
public void Approve(string byUser) => SetUpdatedBy(byUser);
|
|
200
|
+
|
|
201
|
+
[CrudAction(Verb = CrudActionVerb.Put)]
|
|
202
|
+
public void Discontinue() => IsDiscontinued = true;
|
|
203
|
+
|
|
204
|
+
[CrudAction("supplier-reference", Verb = CrudActionVerb.Put)]
|
|
205
|
+
public void AssignSupplierReference([Required, StringLength(50)] string supplierReferenceCode) =>
|
|
206
|
+
SupplierReferenceCode = supplierReferenceCode;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**`Product` is not `sealed`.** No architecture test enforces sealing on `Minimal.Domains` entities
|
|
211
|
+
(only on mappers/handlers/validators), and nothing subclasses it — an inconsistency, not a rule to
|
|
212
|
+
copy. Default to `sealed` unless you have a concrete reason not to, as `mode=manual` does.
|
|
213
|
+
|
|
214
|
+
### Events: `[RaisesEvent]`
|
|
215
|
+
|
|
216
|
+
Class-level, repeatable. This template uses only the label-less convention form:
|
|
217
|
+
`[RaisesEvent(operations, params string[] properties)]`, with `Include`/`Exclude` narrowing the
|
|
218
|
+
auto-composed payload. Two other forms exist in the package (type-naming, label) but neither sample
|
|
219
|
+
uses them.
|
|
220
|
+
|
|
221
|
+
- `EventOperations`: `Created`, `Updated`, `Deleted`.
|
|
222
|
+
- `Include`/`Exclude` (mutually exclusive) shape the auto-composed payload record —
|
|
223
|
+
`Include = [nameof(Id), nameof(Name), nameof(Price)]` on the `Created` rule means
|
|
224
|
+
`ProductCreatedEvent` carries exactly those three properties.
|
|
225
|
+
- The `string[] properties` argument narrows an `Updated` rule: non-empty raises only when a listed
|
|
226
|
+
property's value actually changed (change-tracker original value, not whether the setter ran) —
|
|
227
|
+
calling `ChangePrice` with its current price does not raise `ProductPriceUpdatedEvent`. Empty means
|
|
228
|
+
any change qualifies. A property list on a `Created`-only rule is ignored with a build warning.
|
|
229
|
+
- **Composed name**: entity name + narrowing properties + operation + `Event`. On `Product`:
|
|
230
|
+
`[RaisesEvent(EventOperations.Created, Include = [...])]` → `ProductCreatedEvent`;
|
|
231
|
+
`[RaisesEvent(EventOperations.Updated, nameof(Price))]` → `ProductPriceUpdatedEvent`, **not**
|
|
232
|
+
`ProductUpdatedEvent`; `[RaisesEvent(EventOperations.Updated, nameof(IsDiscontinued))]` →
|
|
233
|
+
`ProductIsDiscontinuedUpdatedEvent`. This is what lets two `Updated` rules on one entity coexist.
|
|
234
|
+
- None of the three has a hand-written source file — they're compiled output. Verify a composed name
|
|
235
|
+
against the built assembly before wiring a consumer to it:
|
|
236
|
+
```bash
|
|
237
|
+
strings ApiEndpoints/Minimal.Domains/bin/Release/net10.0/Minimal.Domains.dll | grep Event
|
|
238
|
+
```
|
|
239
|
+
- `[RaisesEvent]` alone raises nothing — the host must register `DKNet.EfCore.Events`' save hook
|
|
240
|
+
(`AddSlimBusEfCoreInterceptor<CoreDbContext>()`, already wired in this template) before declared
|
|
241
|
+
events publish.
|
|
242
|
+
- Never both styles for the same logical change: pick `AddEvent` (mode=manual) or `[RaisesEvent]`
|
|
243
|
+
(mode=auto) for a given entity's events, not a mix on one property. Writing a consumer for a
|
|
244
|
+
raised event (either style) is covered by `dknet-messaging-events`, not this skill.
|
|
245
|
+
|
|
246
|
+
### Generator attributes: `[CrudCreate]`, `[CrudUpdate]`, `[CrudAction]`
|
|
247
|
+
|
|
248
|
+
All three live in `DKNet.EfCore.Abstractions.Attributes`. Mechanically identical: the annotated
|
|
249
|
+
member's parameter list *is* the generated request's payload, and its `DataAnnotations`
|
|
250
|
+
(`[Required]`, `[StringLength]`, `[Range]`) forward 1:1 onto the generated request property — but see
|
|
251
|
+
`dknet-crud`/`dknet-endpoint` for whether that attribute is actually **enforced** on the mapped route.
|
|
252
|
+
|
|
253
|
+
| | `[CrudCreate]` | `[CrudUpdate]` | `[CrudAction]` |
|
|
254
|
+
|---|---|---|---|
|
|
255
|
+
| Placement | one constructor | a method | a method |
|
|
256
|
+
| Generates | `Create<Entity>Request` + handler | `Change<Member><Entity>Request` + handler, `PUT {id}` | `<Method><Entity>Request` + handler, `{Verb} {id}/{segment}` |
|
|
257
|
+
| Verb | `POST` (create route) | `PUT` | `POST` default, override to `Put`/`Patch` (`CrudActionVerb`) |
|
|
258
|
+
| Route segment | — | — | method name kebab-cased, or `[CrudAction("segment")]` |
|
|
259
|
+
| Response | `201` + DTO | `200` + DTO | `200` + DTO — never `204` |
|
|
260
|
+
| Acting-user param | never — see below | never | fine (see `Approve(string byUser)`), but see the pitfall below |
|
|
261
|
+
|
|
262
|
+
One `[CrudUpdate]` method changes exactly the fields in its own parameter list — a method needing to
|
|
263
|
+
change two independent fields together needs two `[CrudUpdate]` methods, not one bigger request.
|
|
264
|
+
|
|
265
|
+
**Action vs. update** — decide by whether repeating the call is safe. `[CrudUpdate]`: caller supplies
|
|
266
|
+
a value and the row ends up holding it, calling twice is the same as once (`ChangePrice(decimal)`).
|
|
267
|
+
`[CrudAction]`: repetition isn't automatically safe (approve, discontinue, re-issue) — `POST` is the
|
|
268
|
+
default because it promises nothing about retries. `Product.Approve` was originally `[CrudUpdate]`,
|
|
269
|
+
over-promising retry-safety; moving it to `[CrudAction("approval")]` fixed the contract without
|
|
270
|
+
changing the method body. Drop a generated action for a hand-written route only when the operation
|
|
271
|
+
writes more than one aggregate in one transaction (`Product.Discontinue`, which also creates a
|
|
272
|
+
replacement product — see `dknet-crud`/`dknet-endpoint`).
|
|
273
|
+
|
|
274
|
+
**`[CrudCreate]` never gets an acting-user parameter.** The generated create request's shape is the
|
|
275
|
+
constructor's parameter list, verbatim — a `createdBy`/`byUser` constructor parameter would become a
|
|
276
|
+
caller-settable body field. `Product`'s `[CrudCreate]` constructor takes only `name`, `price`,
|
|
277
|
+
`supplierCostPrice`; `CreatedBy`/`OwnedBy` are stamped at save time from the authenticated caller
|
|
278
|
+
instead (`DataOwnerHook`, `DKNet.EfCore.AuditLogs`' audit hook — see `dknet-efcore-config`).
|
|
279
|
+
|
|
280
|
+
**Pitfall — a `[CrudAction]` method's `byUser` parameter is caller-settable.** `Product.Approve(string
|
|
281
|
+
byUser)` becomes a generated `ApproveProductRequest` with a `required string ByUser` **body**
|
|
282
|
+
property — any caller can set it to any value; nothing populates it from the authenticated principal
|
|
283
|
+
the way `[FromClaim]` does for a hand-written request. Only use an action parameter named like an
|
|
284
|
+
acting-user field when that is genuinely intended as caller-supplied data, never to attribute the
|
|
285
|
+
call.
|
|
286
|
+
|
|
287
|
+
### `[SensitiveData]`
|
|
288
|
+
|
|
289
|
+
`DKNet.EfCore.Abstractions.Attributes.SensitiveDataAttribute`, on a property. Two effects: unconditional
|
|
290
|
+
redaction in `DKNet.EfCore.AuditLogs` audit-log capture (not wired here), and role-gated JSON
|
|
291
|
+
serialization once the host opts in (`UseRoleAwareSensitiveData` — see `dknet-endpoint`).
|
|
292
|
+
`[SensitiveData("pricing")]` — only a caller in that role (any one of several named) gets the value
|
|
293
|
+
serialized; `[SensitiveData]` with no role — any *authenticated* caller gets it, unauthenticated is
|
|
294
|
+
refused. The attribute alone changes nothing until the serializer opts in. `DKNet.EfCore.DtoGenerator`
|
|
295
|
+
copies it onto the matching generated DTO property automatically — never re-declare it by hand.
|
|
296
|
+
|
|
297
|
+
### `IOwnedBy` — row-level isolation
|
|
298
|
+
|
|
299
|
+
Implement `IOwnedBy { string OwnedBy { get; } }` (`DKNet.EfCore.DataAuthorization`) to subject an
|
|
300
|
+
entity to the global read filter — a caller sees only rows whose `OwnedBy` matches their own
|
|
301
|
+
ownership key. `DataOwnerHook` stamps it on insert from `IDataOwnerProvider.GetOwnershipKey()` and
|
|
302
|
+
refuses reassignment to a key the caller doesn't hold. The mapper must still size the column by hand
|
|
303
|
+
— `builder.Property(p => p.OwnedBy).HasMaxLength(500).IsRequired();` (see `dknet-efcore-config`) —
|
|
304
|
+
the interface alone doesn't do it.
|
|
305
|
+
|
|
306
|
+
## Domain services, sequences, and `DomainSchemas`
|
|
307
|
+
|
|
308
|
+
`Minimal.Domains/Share/DomainSchemas.cs` holds named schema constants (`Migration = "migrate"`,
|
|
309
|
+
`Profile = "pro"`) for reuse across mappers — neither sample uses one; both pass a literal schema
|
|
310
|
+
string to `ToTable(...)` instead. Add a constant only when a schema name is reused by >1 entity.
|
|
311
|
+
|
|
312
|
+
`Minimal.Domains/Share/Sequences.cs` declares named PostgreSQL sequences:
|
|
313
|
+
|
|
314
|
+
```csharp
|
|
315
|
+
[SqlSequence]
|
|
316
|
+
public enum Sequences
|
|
317
|
+
{
|
|
318
|
+
None = 0,
|
|
319
|
+
|
|
320
|
+
[Sequence(typeof(int), FormatString = "T{DateTime:yyMMdd}{1:00000}", Max = 99999)]
|
|
321
|
+
Membership = 1
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The domain-service contract pattern that wraps a sequence (`Minimal.Domains/Services/`):
|
|
326
|
+
|
|
327
|
+
```csharp
|
|
328
|
+
public interface IDomainService; // marker, no members
|
|
329
|
+
public interface ISequenceServices : IDomainService
|
|
330
|
+
{
|
|
331
|
+
ValueTask<string> NextValueAsync();
|
|
332
|
+
}
|
|
333
|
+
public interface IMembershipService : ISequenceServices; // one sequence, one interface
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The `Minimal.Infra` implementation is a one-line primary-constructor subclass of an internal
|
|
337
|
+
`SequenceService` base (see `dknet-efcore-config`). Neither sample uses a sequence; add one the same
|
|
338
|
+
way `IMembershipService`/`MembershipService` do for `Sequences.Membership`, for entities needing a
|
|
339
|
+
human-readable sequential id instead of a `Guid`.
|
|
340
|
+
|
|
341
|
+
## Owned value objects
|
|
342
|
+
|
|
343
|
+
Not exercised by either sample — no `OwnsOne`/`OwnsMany` call exists anywhere in this solution. If a
|
|
344
|
+
feature genuinely needs one, the minimal pattern is a plain class with no independent identity,
|
|
345
|
+
configured in the entity's own mapper (`dknet-efcore-config`):
|
|
346
|
+
|
|
347
|
+
```csharp
|
|
348
|
+
public sealed class Address
|
|
349
|
+
{
|
|
350
|
+
public string Street { get; private set; } = null!;
|
|
351
|
+
public string City { get; private set; } = null!;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// in the entity's IEntityTypeConfiguration<T>.Configure, after base.Configure(builder):
|
|
355
|
+
builder.OwnsOne(e => e.ShippingAddress, owned =>
|
|
356
|
+
{
|
|
357
|
+
owned.Property(p => p.Street).HasMaxLength(200).IsRequired();
|
|
358
|
+
owned.Property(p => p.City).HasMaxLength(100).IsRequired();
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Architecture tests that constrain `Minimal.Domains`
|
|
363
|
+
|
|
364
|
+
`Architecture/RecordArchitectureTests.cs` scans `Minimal.Domains`/`Minimal.AppServices` for any
|
|
365
|
+
record with a public property that has a **private** setter, and fails — Mapster/AutoMapper can't
|
|
366
|
+
map those. A hand-written event record (`public sealed record XCreatedEvent(...)`) is positional and
|
|
367
|
+
passes by construction. `Architecture/SampleInvariantTests.cs` enforces the two-sample split:
|
|
368
|
+
`ManualSample_ShouldNotUseAnyDeclarativeGenerationAttribute` fails on any `ManualSample` file
|
|
369
|
+
containing `[RaisesEvent`, `[CrudCreate]`, `[CrudUpdate]`, `[CrudAction`, or `[GenerateDto`;
|
|
370
|
+
`AutomatedSample_ShouldNotRaiseEventsByHand` fails on any `AutomatedSample` file calling `AddEvent(`.
|
|
371
|
+
No architecture test enforces `sealed`, visibility, or constructor shape on an entity directly — the
|
|
372
|
+
conventions above are followed by convention, not test (unlike mapper/handler/validator sealing).
|
|
373
|
+
|
|
374
|
+
## Unit tests to mirror
|
|
375
|
+
|
|
376
|
+
- `Minimal.App.Tests/Unit/ManualSample/PurchaseOrderTests.cs` — constructor sets properties and
|
|
377
|
+
raises `PurchaseOrderCreatedEvent` (asserted via `order.GetEvents()`); the rehydration constructor
|
|
378
|
+
does not raise it; mutation methods stamp `UpdatedBy`.
|
|
379
|
+
- `Minimal.App.Tests/Unit/AutomatedSample/ProductTests.cs` — constructor sets properties (no event
|
|
380
|
+
assertion — declared events don't raise outside a real `SaveChanges`, see
|
|
381
|
+
`dknet-messaging-events`); reflection asserts `[CrudCreate]`/`[CrudUpdate]` `DataAnnotations` are
|
|
382
|
+
present, the only thing a unit test can prove about a forwarded-but-unenforced attribute;
|
|
383
|
+
`Discontinue` called twice stays discontinued — the repeat-call refusal lives in the handler, not
|
|
384
|
+
the entity method.
|
|
385
|
+
|
|
386
|
+
## Step-by-step
|
|
387
|
+
|
|
388
|
+
**mode=manual**
|
|
389
|
+
|
|
390
|
+
1. Create `ApiEndpoints/Minimal.Domains/Features/<Feature>/Entities/<Entity>.cs` following the
|
|
391
|
+
three-constructor shape above, `AddEvent(new <Entity>CreatedEvent(...))` in the public one.
|
|
392
|
+
2. Add `public sealed record <Entity>CreatedEvent(...)` next to the entity.
|
|
393
|
+
3. Mark the class `sealed` unless you have a specific reason not to.
|
|
394
|
+
4. Continue to `dknet-efcore-config` for the mapper.
|
|
395
|
+
|
|
396
|
+
**mode=auto**
|
|
397
|
+
|
|
398
|
+
1. Create `ApiEndpoints/Minimal.Domains/Features/<Feature>/Entities/<Entity>.cs` following the
|
|
399
|
+
attribute shape above (`[RaisesEvent]`, `[CrudCreate]`, `[CrudUpdate]`, `[CrudAction]`, `IOwnedBy`
|
|
400
|
+
as needed).
|
|
401
|
+
2. Build once, inspect `obj/Generated/DKNet.SlimBus.Generators/…` for request/handler names and
|
|
402
|
+
`strings …/Minimal.Domains.dll | grep Event` for composed event names.
|
|
403
|
+
3. Continue to `dknet-efcore-config` for the mapper (unchanged by this mode).
|
|
404
|
+
|
|
405
|
+
## mode=manual vs mode=auto — when to pick which
|
|
406
|
+
|
|
407
|
+
Full trade-off table lives in `dknet-feature-lifecycle`; short version: pick `mode=auto` for a plain
|
|
408
|
+
CRUD-shaped entity where built-in `Created`/`Updated`-on-change semantics and generated routes are
|
|
409
|
+
enough (a `FluentValidation` validator against a generated request still runs — see `dknet-crud`).
|
|
410
|
+
Pick `mode=manual` when construction/mutation needs logic beyond setting fields, an operation spans
|
|
411
|
+
more than one aggregate, or a domain method needs the acting user's identity as data.
|
|
412
|
+
|
|
413
|
+
## Validation checklist
|
|
414
|
+
|
|
415
|
+
- [ ] Entity ultimately derives from `AggregateRoot` (or `DomainEntity` for a non-root entity)
|
|
416
|
+
- [ ] Every property is `{ get; private set; }` — no public setters
|
|
417
|
+
- [ ] mode=manual: three constructors (public, `internal` rehydration, private parameterless);
|
|
418
|
+
mode=auto: `[CrudCreate]` constructor with no acting-user parameter
|
|
419
|
+
- [ ] mode=manual: `AddEvent(...)` called in the constructor, event is a `public sealed record` next
|
|
420
|
+
to the entity; mode=auto: `[RaisesEvent]` declared at class level, composed name verified via
|
|
421
|
+
`strings` against the built DLL
|
|
422
|
+
- [ ] Every mutation method (mode=manual) or `[CrudUpdate]`/`[CrudAction]` method (mode=auto) that
|
|
423
|
+
changes state ends by stamping the acting user (`SetUpdatedBy` for manual; auto's hooks do this
|
|
424
|
+
at save time — don't call `SetUpdatedBy` yourself unless the method needs to record it as
|
|
425
|
+
domain data, as `Approve` does)
|
|
426
|
+
- [ ] `IOwnedBy` implemented only when the feature needs row-level isolation, and the mapper sizes
|
|
427
|
+
`OwnedBy` explicitly
|
|
428
|
+
- [ ] No mixing of `AddEvent` and `[RaisesEvent]` for the same entity
|
|
429
|
+
- [ ] `dotnet build -c Release` passes
|
|
430
|
+
|
|
431
|
+
## Common mistakes
|
|
432
|
+
|
|
433
|
+
| Mistake | Fix |
|
|
434
|
+
|---|---|
|
|
435
|
+
| Adding a `createdBy`/`byUser` constructor parameter to a `[CrudCreate]` constructor | Never — it becomes a caller-settable field on the generated create request. Stamp the acting user from the authenticated caller at save time instead. |
|
|
436
|
+
| Naming a `[CrudAction]` parameter `byUser` and expecting it to carry the authenticated caller | **What you might expect:** it's populated like `[FromClaim]`. **What actually happens:** it's an ordinary caller-settable body field on the generated request. **Why:** the generator forwards only the parameter list and its `DataAnnotations`; it has no concept of `[FromClaim]`. |
|
|
437
|
+
| Assuming `ChangePrice`'s `[Range(0.01, double.MaxValue)]` is enforced because it's on the entity | It's forwarded onto the generated request but only *enforced* when the route is a literal `Map*(string, Delegate)` call — generated CRUD routes aren't. See `dknet-crud`. |
|
|
438
|
+
| Guessing a composed `[RaisesEvent]` name from the pattern alone | Always verify with `strings ApiEndpoints/Minimal.Domains/bin/Release/net10.0/Minimal.Domains.dll \| grep Event` after building — there's no source file to read. |
|
|
439
|
+
| Expecting an `Updated` rule to fire because a setter ran | It only fires when the named property's value actually changed relative to the change tracker's original value on that save. |
|
|
440
|
+
| Sealing `Product`-style logic because "the manual sample is sealed" | `sealed` is a good default but is not enforced on `Minimal.Domains` entities by any architecture test — don't cite one that doesn't exist. |
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
# Workflow: `/dknet-entity`
|
|
445
|
+
|
|
446
|
+
The procedure an agent follows when invoked with arguments. The reference sections above are the rules it applies.
|
|
447
|
+
|
|
448
|
+
You are scaffolding the **Domain + Infra** layers of a vertical slice. Stop after the migration is generated and the solution builds — `/dknet-crud` will continue from there.
|
|
449
|
+
|
|
450
|
+
### Inputs
|
|
451
|
+
|
|
452
|
+
`$ARGUMENTS` — feature folder (plural, PascalCase), aggregate name (singular, PascalCase), an optional
|
|
453
|
+
`mode=manual|auto` (defaults to `manual`), and an optional list of properties (`Name:Type`, append `?`
|
|
454
|
+
for nullable).
|
|
455
|
+
|
|
456
|
+
**The mode changes what this command writes onto the entity.** In `auto` the entity itself carries the
|
|
457
|
+
CRUD and event surface as attributes — skipping them here makes `/dknet-crud mode=auto` unreachable,
|
|
458
|
+
because the generator has nothing to read. If the mode was not supplied, apply
|
|
459
|
+
the `dknet-feature-lifecycle` skill §1 to choose one and say which you chose.
|
|
460
|
+
|
|
461
|
+
### Required reading
|
|
462
|
+
|
|
463
|
+
1. The reference sections above (mode=manual / mode=auto sections cover the exemplar shapes in full)
|
|
464
|
+
2. the `dknet-efcore-config` skill
|
|
465
|
+
3. `manual` exemplar — `ApiEndpoints/Minimal.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`; `auto` exemplar — `ApiEndpoints/Minimal.Domains/Features/AutomatedSample/Entities/Product.cs`
|
|
466
|
+
4. `ApiEndpoints/Minimal.Infra/Features/ManualSample/Mappers/PurchaseOrderConfigs.cs` (exemplar mapper — hand-written in **both** modes; no generator produces this)
|
|
467
|
+
|
|
468
|
+
### Steps
|
|
469
|
+
|
|
470
|
+
1. Use the `dknet-implementer` subagent (via the Agent tool) to execute Steps 1–4 of the implementer protocol: domain entity, schema constant, owned types, EF Core mapper, optional sequence/seed data, then `dotnet ef migrations add <Name> -c CoreDbContext -p Minimal.Infra/Minimal.Infra.csproj` from `ApiEndpoints/`. Apply the mode=manual or mode=auto rules from the sections above verbatim — don't re-derive them.
|
|
471
|
+
2. Run `dotnet build -c Release` and stop on first error.
|
|
472
|
+
3. **`auto` only** — verify composed event-record names against the built DLL (see "Events: `[RaisesEvent]`" above) before wiring a consumer: `strings ApiEndpoints/Minimal.Domains/bin/Release/net10.0/Minimal.Domains.dll | grep <Entity>`.
|
|
473
|
+
4. Report:
|
|
474
|
+
- mode used,
|
|
475
|
+
- files created (relative paths),
|
|
476
|
+
- migration name + tables/indexes,
|
|
477
|
+
- for `auto`, the composed event-record names you verified,
|
|
478
|
+
- the exact next command (`/dknet-crud <Feature> <Entity> mode=<mode>`).
|
|
479
|
+
|
|
480
|
+
### Constraints
|
|
481
|
+
|
|
482
|
+
- Do NOT touch `Minimal.AppServices` or `Minimal.Api` here — those belong to `/dknet-crud` and `/dknet-endpoint`.
|
|
483
|
+
- Entity/mapper/event rules: see Validation checklist above — verify against it, don't restate it.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dknet-feature
|
|
3
|
+
description: Drive an end-to-end DKNet vertical-slice feature from plan to merged tests in either the manual or automated flow — orchestrates entity, CRUD, endpoint, tests, BDD, and docs.
|
|
4
|
+
metadata:
|
|
5
|
+
kind: workflow
|
|
6
|
+
arguments: "<Feature> <Entity> [mode=manual|auto] [props…] e.g. Orders Order mode=manual Number:string Total:decimal"
|
|
7
|
+
allowed-tools: Read, Grep, Glob, Edit, Write, Bash, Agent, TodoWrite
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Usage: `/dknet-feature <Feature> <Entity> [mode=manual|auto] [props…] e.g. Orders Order mode=manual Number:string Total:decimal`
|
|
11
|
+
|
|
12
|
+
You are the **DKNet Feature Orchestrator**. You take a feature request and drive it across every layer of a DKNet.Minimal solution to a working, tested, documented vertical slice. You delegate aggressively to subagents and skill-bound slash commands; you do not personally write product code.
|
|
13
|
+
|
|
14
|
+
To retire a feature, use `/dknet-feature-remove <Feature>`.
|
|
15
|
+
|
|
16
|
+
## Inputs
|
|
17
|
+
|
|
18
|
+
`$ARGUMENTS` — feature folder (plural PascalCase), aggregate name (singular PascalCase), an optional
|
|
19
|
+
`mode=manual|auto`, and an optional property list. Example:
|
|
20
|
+
```
|
|
21
|
+
/dknet-feature Orders Order mode=manual Number:string Total:decimal CustomerId:Guid Status:OrderStatus
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If the input is ambiguous, STOP and ask before doing anything else.
|
|
25
|
+
|
|
26
|
+
## Required reading
|
|
27
|
+
|
|
28
|
+
The `dknet-feature-lifecycle` skill — §1 flow selection, §2 footprint. Read before Phase 0.
|
|
29
|
+
|
|
30
|
+
## Phase 0 — Flow selection
|
|
31
|
+
|
|
32
|
+
The **mode** decides what every later phase produces. The two flows generate a different set of
|
|
33
|
+
files, different validation behavior, and different acting-user attribution — they are not two styles
|
|
34
|
+
of the same output.
|
|
35
|
+
|
|
36
|
+
| Mode | Exemplar | Shape |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `manual` | `ManualSample` / `PurchaseOrder` | Every request, validator, handler, spec, DTO, and route is a file you write. Enforced validation, `.RequiredIdempotentKey()` on create, `[FromClaim]` acting user. |
|
|
39
|
+
| `auto` | `AutomatedSample` / `Product` | `[RaisesEvent]` / `[CrudCreate]` / `[CrudUpdate]` / `[CrudAction]` on the entity plus a one-line `[GenerateDto]`. Requests, handlers, and routes are generated. No idempotency, **forwarded DataAnnotations not enforced** (a FluentValidation validator on a generated request *is*), acting user via `DataOwnerHook`. |
|
|
40
|
+
|
|
41
|
+
If `mode=` was not supplied, apply §1 of the lifecycle skill, **recommend one with a reason**, and ask
|
|
42
|
+
the user to confirm. Default to `manual` whenever the request mentions an operation that writes more
|
|
43
|
+
than one aggregate in one transaction, idempotent writes, a filtered query, a DTO that hides fields,
|
|
44
|
+
or attribute-declared validation that must return `400` — `auto` is a deliberate trade, not a
|
|
45
|
+
fallback. A business rule, state transition or duplicate check on its own does **not** force
|
|
46
|
+
`manual`: write it as a FluentValidation validator on the generated request, the way the product
|
|
47
|
+
sample refuses a duplicate name and a delete of a product still for sale.
|
|
48
|
+
|
|
49
|
+
When `auto` is selected, state the validation gap in your confirmation message: a `[Range]` on a
|
|
50
|
+
generated request property is forwarded but never enforced, so a `POST` with an invalid value returns
|
|
51
|
+
`201`, not `400`. The gap is the attribute only — FluentValidation still runs on generated routes. The user accepts that before Phase 2 starts.
|
|
52
|
+
|
|
53
|
+
Thread the resolved mode into every phase below and do not let it drift. Mixing flows on one
|
|
54
|
+
aggregate is out of scope for this command — if only one operation needs a rule, finish in `auto` and
|
|
55
|
+
report that operation as a follow-up to hand-write.
|
|
56
|
+
|
|
57
|
+
## Workflow (do not skip phases, do not reorder)
|
|
58
|
+
|
|
59
|
+
For each phase: announce the phase, dispatch the work, wait for completion, then verify before moving on. Use `TodoWrite` to track phase status.
|
|
60
|
+
|
|
61
|
+
### Phase 1 — Plan (read-only)
|
|
62
|
+
|
|
63
|
+
Dispatch the `dknet-architect` subagent with the feature request **and the resolved mode**. Print its plan and ask the user to confirm or amend before any code is written. If the user changes the plan, re-run the architect with the amendments.
|
|
64
|
+
|
|
65
|
+
If the architect's plan surfaces a rule that `auto` cannot enforce, say so and re-open Phase 0 rather than carrying the mismatch forward.
|
|
66
|
+
|
|
67
|
+
### Phase 2 — Domain + Infra
|
|
68
|
+
|
|
69
|
+
Run `/dknet-entity <Feature> <Entity> mode=<mode> <props…>`. Verify in both modes:
|
|
70
|
+
- entity inherits `AggregateRoot`, properties `{ get; private set; }`,
|
|
71
|
+
- mapper is `internal sealed : DefaultEntityTypeConfiguration<T>`,
|
|
72
|
+
- `DomainSchemas.<Feature>` constant exists,
|
|
73
|
+
- migration was generated,
|
|
74
|
+
- `dotnet build` is green.
|
|
75
|
+
|
|
76
|
+
Additionally verify, by mode:
|
|
77
|
+
- `manual` — mutation methods raise events via `AddEvent(...)`; a hand-written event record exists.
|
|
78
|
+
- `auto` — class-level `[RaisesEvent(...)]`, a `[CrudCreate]` constructor, and at least one
|
|
79
|
+
`[CrudUpdate]` method are present. No `AddEvent` call anywhere in the slice.
|
|
80
|
+
|
|
81
|
+
### Phase 3 — AppServices CRUD
|
|
82
|
+
|
|
83
|
+
Run `/dknet-crud <Feature> <Entity> mode=<mode>`. Verify:
|
|
84
|
+
- `manual` — Create/Update/Delete requests + validators + `internal sealed` handlers, `SpecGet<Entity>`, queries, hand-written DTO record, event handler.
|
|
85
|
+
- `auto` — exactly one `[GenerateDto(typeof(<Entity>))] public sealed partial record <Entity>Dto;` and any hand-written event *consumer*. Then `dotnet build` and confirm the expected types appeared under `obj/Generated/DKNet.SlimBus.Generators/`. An empty generated folder means the attributes did not take — STOP.
|
|
86
|
+
|
|
87
|
+
Build green either way.
|
|
88
|
+
|
|
89
|
+
### Phase 4 — Endpoint
|
|
90
|
+
|
|
91
|
+
Run `/dknet-endpoint <Feature> <Entity> mode=<mode>`. Verify the new `*V1Endpoint : IEndpointConfig` exists and:
|
|
92
|
+
- `manual` — every route is a literal `group.MapPost/MapGet/MapPut/MapDelete(...)` call, and the create route chains `.RequiredIdempotentKey()`.
|
|
93
|
+
- `auto` — the body is a single `group.Map<Entity>Crud()` call. There is no `.RequiredIdempotentKey()` on this path; do not add one, it will not compile onto the generated route.
|
|
94
|
+
|
|
95
|
+
Build green.
|
|
96
|
+
|
|
97
|
+
### Phase 5 — Unit/integration tests
|
|
98
|
+
|
|
99
|
+
Run `/dknet-unit-tests <Feature> <Entity> mode=<mode>`. Verify all tests pass and cover happy path, not-found, and domain events in both modes, plus by mode:
|
|
100
|
+
- `manual` — FluentValidation failures, duplicate detection, and any rejected state transition.
|
|
101
|
+
- `auto` — entity-method behavior directly. Do **not** write a test asserting a `400` from a forwarded DataAnnotations attribute; it will return `201` and the test would encode the gap as expected behavior.
|
|
102
|
+
|
|
103
|
+
### Phase 6 — BDD acceptance tests
|
|
104
|
+
|
|
105
|
+
Dispatch the `dknet-bdd-engineer` subagent with the feature scope. Verify `.feature` + step files exist with status + shape + key-field assertions and the BDD project passes.
|
|
106
|
+
|
|
107
|
+
### Phase 7 — Feature documentation
|
|
108
|
+
|
|
109
|
+
Run `/dknet-docs <Feature>`. Verify README, architecture diagrams, data-model, and api-reference exist under `docs/features/<feature-kebab>/` (or `docs/<feature>/` if internal).
|
|
110
|
+
|
|
111
|
+
### Phase 8 — Final gates
|
|
112
|
+
|
|
113
|
+
1. `dotnet build -c Release` — zero warnings (warnings-as-errors).
|
|
114
|
+
2. `dotnet test --settings coverage.runsettings` — all green.
|
|
115
|
+
3. Print a final report:
|
|
116
|
+
- Mode used, and the one-line reason it was chosen.
|
|
117
|
+
- Files created/edited grouped by layer.
|
|
118
|
+
- Migration name + tables.
|
|
119
|
+
- Endpoints (route + verbs), flagging whether the create route is idempotent.
|
|
120
|
+
- Test counts (unit + BDD).
|
|
121
|
+
- Docs paths.
|
|
122
|
+
- For `auto`: the exact generated type names produced, and a plain statement that the forwarded
|
|
123
|
+
DataAnnotations validation on those routes is not enforced.
|
|
124
|
+
- `/dknet-feature-remove <Feature>` as the way to retire the slice.
|
|
125
|
+
- Suggested commit/PR title (do not commit unless the user asks).
|
|
126
|
+
|
|
127
|
+
## Stop conditions
|
|
128
|
+
|
|
129
|
+
- Any phase produces a build or test failure that the implementer cannot trivially fix → STOP, summarize, ask the user.
|
|
130
|
+
- The architect surfaces an ambiguity → STOP at end of Phase 1, do not start Phase 2.
|
|
131
|
+
- Mode is unresolved or the user has not accepted the `auto` validation gap → STOP at Phase 0.
|
|
132
|
+
- `auto` was selected but the build produces no generated types → STOP; the attributes are wrong and every later phase would build on nothing.
|
|
133
|
+
- The user says "stop" or pivots → halt and report state.
|
|
134
|
+
|
|
135
|
+
## Constraints
|
|
136
|
+
|
|
137
|
+
- Never skip the plan phase. Even when the user gives a clear request, surface the architect's plan for explicit confirmation before writing code.
|
|
138
|
+
- Never commit, push, or open PRs unless the user explicitly asks. The orchestrator's final output is a green test suite and a suggested commit message — not an actual commit.
|
|
139
|
+
- Never edit agent skill/plugin folders or `Directory.Packages.props` as part of a feature slice — those are template-level concerns.
|