@drunkcoding/dknet-implementation-skills 14.1.0 → 14.2.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 (35) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +37 -26
  3. package/package.json +2 -3
  4. package/{.claude-plugin → plugin/.claude-plugin}/plugin.json +1 -1
  5. package/{agents → plugin/agents}/dknet-architect.md +4 -4
  6. package/{agents → plugin/agents}/dknet-bdd-engineer.md +6 -6
  7. package/{agents → plugin/agents}/dknet-implementer.md +11 -11
  8. package/{skills → plugin/skills}/README.md +13 -12
  9. package/{skills → plugin/skills}/dknet-auth-and-ownership/SKILL.md +100 -36
  10. package/{skills → plugin/skills}/dknet-bdd-tests/SKILL.md +25 -16
  11. package/{skills → plugin/skills}/dknet-bdd-tests/checklist.md +1 -1
  12. package/{skills → plugin/skills}/dknet-crud/SKILL.md +36 -22
  13. package/{skills → plugin/skills}/dknet-ddd-principles/SKILL.md +62 -13
  14. package/{skills → plugin/skills}/dknet-docs/SKILL.md +73 -31
  15. package/{skills → plugin/skills}/dknet-docs/templates/README-template.md +8 -8
  16. package/{skills → plugin/skills}/dknet-docs/templates/api-reference-template.md +7 -0
  17. package/{skills → plugin/skills}/dknet-docs/templates/architecture-template.md +14 -8
  18. package/{skills → plugin/skills}/dknet-docs/templates/data-model-template.md +2 -2
  19. package/{skills → plugin/skills}/dknet-docs/templates/events-template.md +2 -2
  20. package/{skills → plugin/skills}/dknet-dto-mapping/SKILL.md +37 -29
  21. package/{skills → plugin/skills}/dknet-efcore-config/SKILL.md +38 -38
  22. package/{skills → plugin/skills}/dknet-endpoint/SKILL.md +72 -43
  23. package/{skills → plugin/skills}/dknet-entity/SKILL.md +43 -33
  24. package/{skills → plugin/skills}/dknet-feature/SKILL.md +36 -13
  25. package/{skills → plugin/skills}/dknet-feature-lifecycle/SKILL.md +48 -42
  26. package/{skills → plugin/skills}/dknet-feature-remove/SKILL.md +16 -15
  27. package/{skills → plugin/skills}/dknet-messaging-events/SKILL.md +36 -29
  28. package/{skills → plugin/skills}/dknet-package-adoption/SKILL.md +3 -3
  29. package/{skills → plugin/skills}/dknet-platform-config/SKILL.md +14 -14
  30. package/{skills → plugin/skills}/dknet-project-structure/SKILL.md +32 -32
  31. package/{skills → plugin/skills}/dknet-queries-specs/SKILL.md +21 -13
  32. package/{skills → plugin/skills}/dknet-scaffold/SKILL.md +3 -2
  33. package/{skills → plugin/skills}/dknet-unit-tests/SKILL.md +27 -22
  34. package/plugin.json +3 -3
  35. /package/{skills → plugin/skills}/dknet-docs/checklist.md +0 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dknet-bdd-tests
3
- description: Create and maintain Reqnroll + NUnit BDD .feature scenarios and step bindings in Minimal.App.BDDTests — request/status/response-body scenarios and domain-event side effects observed via log capture, for the hand-written PurchaseOrder and generator-driven Product samples. Use when adding or updating HTTP-facing scenarios for a DKNet.Templates feature. Result-level Result-object assertions, architecture rules and pure functional tests belong in the `dknet-unit-tests` skill instead — do not duplicate a behavior here that xUnit already covers. Invoke as `/dknet-bdd-tests <Feature>` to scaffold it for a feature.
3
+ description: Create and maintain Reqnroll + NUnit BDD .feature scenarios and step bindings in <YourApp>.App.BDDTests — request/status/response-body scenarios and domain-event side effects observed via log capture, for the hand-written PurchaseOrder and generator-driven Product samples. Use when adding or updating HTTP-facing scenarios for a DKNet.Templates feature. Result-level Result-object assertions, architecture rules and pure functional tests belong in the `dknet-unit-tests` skill instead — do not duplicate a behavior here that xUnit already covers. Invoke as `/dknet-bdd-tests <Feature>` to scaffold it for a feature.
4
4
  metadata:
5
5
  kind: workflow
6
6
  arguments: "<Feature> e.g. Orders"
@@ -9,17 +9,23 @@ allowed-tools: Read, Grep, Glob, Edit, Write, Bash
9
9
 
10
10
  Usage: `/dknet-bdd-tests <Feature> e.g. Orders`
11
11
 
12
- # BDD tests (Minimal.App.BDDTests)
12
+ # BDD tests (<YourApp>.App.BDDTests)
13
13
 
14
14
  ## What BDD owns vs xUnit
15
15
 
16
- BDD owns user-facing HTTP behavior: request → status code → response body, and domain-event side effects
17
- observed through captured log lines. xUnit (`dknet-unit-tests`) owns architecture/convention rules, pure
18
- functional tests (entity methods, validators, specs), and result-level integration assertions on the
19
- handler's `IResult`/`IResultBase` object. Schema/model/migration assertions never belong in BDD. Don't
20
- write a BDD scenario for a rule already proven at the `Result` level in xUnit unless it's the one place
21
- that rule is reachable over HTTP — and don't add a duplicate xUnit HTTP test for a rule a BDD scenario
22
- already proves.
16
+ **BDD is the default home for a feature's business rules.** Anything statable as "a caller does X and
17
+ gets Y" — happy paths, refusals, state transitions, precondition conflicts, domain-event side effects
18
+ observed through captured log lines — is a scenario here, because that is the form a team can read as
19
+ the specification of the feature.
20
+
21
+ xUnit (`dknet-unit-tests`) keeps what a scenario cannot express or cannot distinguish:
22
+ architecture/convention rules, pure functional tests (entity methods, validators, specs in isolation),
23
+ EF model/schema/migration shape, and a `Result`-level assertion **only** where the HTTP response does
24
+ not identify which rule fired (two failures sharing one status code, a `NotFoundError` versus an
25
+ ownership filter that also answers `404`). Schema/model/migration assertions never belong in BDD.
26
+
27
+ Never assert the same rule in both suites. If a rule is reachable over HTTP and its response
28
+ identifies it, the scenario is the test — delete the xUnit duplicate rather than keeping both.
23
29
 
24
30
  Both shipped suites are teaching material: every scenario must be about the `PurchaseOrder` (manual) or
25
31
  `Product` (automated) sample's business behavior, never about logging, health checks, CORS, security
@@ -185,7 +191,7 @@ Scenario: A product name that is already taken is refused
185
191
  The log-line step just asserts on `LogCapture.Messages` — no polling needed here because the response body
186
192
  already round-trips through the handler that logs synchronously before returning; if a future scenario
187
193
  asserts on an *async consumer's* log line instead (a domain event's own subscriber, not the handler that
188
- raised it), poll with `Eventually.IsTrueAsync(...)` from `Minimal.App.TestSupport` rather than asserting
194
+ raised it), poll with `Eventually.IsTrueAsync(...)` from `<YourApp>.App.TestSupport` rather than asserting
189
195
  immediately — the in-memory bus publishes non-blocking:
190
196
 
191
197
  ```csharp
@@ -242,7 +248,7 @@ like `"status":"placed"` where the surrounding shape isn't in question.
242
248
  ## Commands
243
249
 
244
250
  ```bash
245
- dotnet test ApiEndpoints/Minimal.App.BDDTests/Minimal.App.BDDTests.csproj --filter "TestCategory=PurchaseOrder"
251
+ dotnet test ApiEndpoints/<YourApp>.App.BDDTests/<YourApp>.App.BDDTests.csproj --filter "TestCategory=PurchaseOrder"
246
252
  ```
247
253
 
248
254
  ## Step-by-step
@@ -302,10 +308,10 @@ Before any BDD design or edits:
302
308
  ### Scope
303
309
 
304
310
  Work only on BDD test artifacts and closely related support wiring:
305
- - `ApiEndpoints/Minimal.App.BDDTests/Features/**/*.feature`
306
- - `ApiEndpoints/Minimal.App.BDDTests/Features/**/Steps/*.cs`
307
- - `ApiEndpoints/Minimal.App.BDDTests/Support/*.cs`
308
- - `ApiEndpoints/Minimal.App.BDDTests/*.csproj`
311
+ - `ApiEndpoints/<YourApp>.App.BDDTests/Features/**/*.feature`
312
+ - `ApiEndpoints/<YourApp>.App.BDDTests/Features/**/Steps/*.cs`
313
+ - `ApiEndpoints/<YourApp>.App.BDDTests/Support/*.cs`
314
+ - `ApiEndpoints/<YourApp>.App.BDDTests/*.csproj`
309
315
 
310
316
  ### Constraints
311
317
 
@@ -324,6 +330,9 @@ Work only on BDD test artifacts and closely related support wiring:
324
330
  forwarded DataAnnotations — a scenario expecting `400` from an out-of-range value will fail against
325
331
  a `201`. Cover that gap by asserting what happens, or leave it to the manual flow.
326
332
  - Do not implement unrelated domain/business logic outside BDD test scope.
333
+ - Cover every business rule the feature exposes over HTTP — happy path, each refusal, each state
334
+ transition. If a rule already has an xUnit `Result`-level test and its HTTP response identifies it
335
+ uniquely, write the scenario here and say in your report that the xUnit duplicate should go.
327
336
 
328
337
  ### Workflow
329
338
 
@@ -338,7 +347,7 @@ Work only on BDD test artifacts and closely related support wiring:
338
347
  3. Implement/adjust step bindings in `Steps/*.cs`.
339
348
  4. Run validation:
340
349
  - `dotnet build -c Release`
341
- - `dotnet test ApiEndpoints/Minimal.App.BDDTests`
350
+ - `dotnet test ApiEndpoints/<YourApp>.App.BDDTests`
342
351
  5. Report:
343
352
  - changed files
344
353
  - scenario count
@@ -34,6 +34,6 @@ Use this checklist before considering BDD scenario work complete.
34
34
  ## Validation
35
35
 
36
36
  - [ ] `dotnet build -c Release` succeeds
37
- - [ ] `dotnet test ApiEndpoints/Minimal.App.BDDTests/Minimal.App.BDDTests.csproj` passes
37
+ - [ ] `dotnet test ApiEndpoints/<YourApp>.App.BDDTests/<YourApp>.App.BDDTests.csproj` passes
38
38
  - [ ] No undefined or pending Reqnroll steps
39
39
  - [ ] Scenario names are readable in test output
@@ -16,14 +16,14 @@ Commands only: Create, Update, a business-rule transition, Delete, and the gener
16
16
  `dknet-queries-specs`. For DTO shape and Mapster wiring, load `dknet-dto-mapping`. For whether a
17
17
  rule belongs on the entity or in a handler, load `dknet-ddd-principles` first.
18
18
 
19
- ## GlobalUsings already in Minimal.AppServices
19
+ ## GlobalUsings already in <YourApp>.AppServices
20
20
 
21
- `Minimal.AppServices/GlobalUsings.cs` gives every file in the project these without an explicit
21
+ `<YourApp>.AppServices/GlobalUsings.cs` gives every file in the project these without an explicit
22
22
  `using`: `DKNet.AspCore.Extensions.ModelBinding` (`[FromClaim]`), `DKNet.SlimBus.Extensions`
23
23
  (`Fluents.*`, `NotFoundError`), `System.ComponentModel.DataAnnotations`, `System.Security.Claims`
24
- (`ClaimTypes`), `System.Text.Json.Serialization`, `FluentResults`, `Minimal.Domains.Services`,
24
+ (`ClaimTypes`), `System.Text.Json.Serialization`, `FluentResults`, `<YourApp>.Domains.Services`,
25
25
  `Microsoft.Extensions.DependencyInjection`, `FluentValidation`, `Mapster`, `MapsterMapper`,
26
- `Minimal.AppServices.Extensions`, `DKNet.SlimBus.Extensions.LazyMapper`, `Minimal.AppServices.Share`
26
+ `<YourApp>.AppServices.Extensions`, `DKNet.SlimBus.Extensions.LazyMapper`, `<YourApp>.AppServices.Share`
27
27
  (`PreconditionCodes`, `IPrincipalProvider`). An action file typically adds only the entity's and its
28
28
  own spec's namespace.
29
29
 
@@ -38,7 +38,7 @@ The handler method is `OnHandle(TRequest, CancellationToken)`, never `Handle`. B
38
38
  by assembly scan onto the in-memory bus — no per-message registration. Handlers never call
39
39
  `SaveChanges`: the SlimBus EF Core interceptor auto-saves once, after the handler returns. A request
40
40
  record is `public sealed record`; its validator and handler are `internal sealed class` — enforced
41
- by `Minimal.App.Tests/Architecture/AppServiceTests.cs`.
41
+ by `<YourApp>.App.Tests/Architecture/AppServiceTests.cs`.
42
42
 
43
43
  File layout: one file per verb, request + validator + handler co-located —
44
44
  `<Feature>/V1/Actions/<Verb>.cs` (`ManualSample/V1/Actions/Create.cs`, `Update.cs`, `Cancel.cs`,
@@ -58,6 +58,10 @@ if (string.IsNullOrEmpty(request.ByUser))
58
58
  }
59
59
  ```
60
60
 
61
+ `[FromRequestHeader("X-Header-Name")]` (same populator, same overwrite-always guarantee) reads a
62
+ named request header instead of a claim — for a correlation id or tenant hint, never for identity: a
63
+ header is caller-supplied and is not an authorization signal.
64
+
61
65
  The demo authentication provider (`FeatureManagement:EnableDemoAuthentication`) supplies this claim
62
66
  locally and in tests, so the guard is reachable but rarely hit — an authenticated caller with a
63
67
  missing name claim still has to fail it explicitly, there is no fallback to a system account.
@@ -88,7 +92,7 @@ from `DataOwnerHook`/the audit hook instead, or hand-write the action.
88
92
  ## FluentValidation
89
93
 
90
94
  A validator is `internal sealed class XValidator : AbstractValidator<TRequest>`, co-located with
91
- its request. There is no per-route opt-in call: `Minimal.Api/Configs/FluentValidationConfig.cs`
95
+ its request. There is no per-route opt-in call: `<YourApp>.Api/Configs/FluentValidationConfig.cs`
92
96
  registers every validator in the assembly —
93
97
 
94
98
  ```csharp
@@ -99,7 +103,7 @@ builder.Services.AddValidatorsFromAssembly(typeof(AppSetup).Assembly, includeInt
99
103
  group (`Program.cs`), so it runs on generated routes exactly as it runs on hand-mapped ones. A
100
104
  request failing validation never reaches the handler; it short-circuits to `400` (or `409`, see
101
105
  below) with no handler code involved. Two hard constraints: the validator class must live in
102
- `Minimal.AppServices` (one in `Minimal.Api` is never registered), and it must be `internal sealed`.
106
+ `<YourApp>.AppServices` (one in `<YourApp>.Api` is never registered), and it must be `internal sealed`.
103
107
 
104
108
  **Ordinary input rules** stay inline (`RuleFor(a => a.Amount).GreaterThan(0)`). Keep `Id` out of an
105
109
  update validator — an unknown/empty id is a `404` from the handler's spec lookup, not a `400` from
@@ -142,9 +146,9 @@ If it refused a missing id too, the route's own `404` would be hidden behind a `
142
146
 
143
147
  **Validators on generated requests are the supported way to add a business rule to a generated
144
148
  route.** `CreateProductRequestValidator` and `DeleteProductRequestValidator` both target generated
145
- request types (`Minimal.AppServices.Crud.CreateProductRequest` / `DeleteProductRequest`) — no
149
+ request types (`<YourApp>.AppServices.Crud.CreateProductRequest` / `DeleteProductRequest`) — no
146
150
  literal `Map*` call is needed for FluentValidation to reach them, only the `using
147
- Minimal.AppServices.Crud;` and a constructor parameter naming the request type. This is the fix for
151
+ <YourApp>.AppServices.Crud;` and a constructor parameter naming the request type. This is the fix for
148
152
  the one thing `[DataAnnotations]` cannot do on a generated route: `[Range]`/`[Required]`/etc.
149
153
  forwarded onto a generated request property is **not evaluated**, because .NET's validation source
150
154
  generator only recognizes literal `Map*(string, Delegate)` calls in the compiling project's own
@@ -229,7 +233,7 @@ Naming, mechanical from the entity's own signatures: `Create<Entity>Request` (fr
229
233
  `<Method><Entity>Request` (from `[CrudAction]`), `Delete<Entity>Request` (always). Handlers mirror
230
234
  the name with `Handler` instead of `Request` — `CreateProductHandler`, `ChangePriceProductHandler`,
231
235
  `ApproveProductHandler`, `DiscontinueProductHandler`, `AssignSupplierReferenceProductHandler` — all
232
- in namespace `Minimal.AppServices.Crud`, emitted under
236
+ in namespace `<YourApp>.AppServices.Crud`, emitted under
233
237
  `obj/Generated/DKNet.SlimBus.Generators/.../<Entity>CrudRequests.g.cs` and `...Handlers.g.cs`. They
234
238
  are compiler output, not files in the repo — inspect them after a build, not by searching source.
235
239
 
@@ -255,7 +259,7 @@ Fetch by a private, generated `ProductByIdCrudSpec`, `404` if missing, call the
255
259
 
256
260
  **Replacing a generated handler.** The generated file's own doc comment says how: "Write a class
257
261
  implementing the same IHandler to replace it." Add your own `internal sealed class` implementing
258
- `Fluents.Requests.IHandler<ChangePriceProductRequest, ProductDto>` in `Minimal.AppServices` — DI
262
+ `Fluents.Requests.IHandler<ChangePriceProductRequest, ProductDto>` in `<YourApp>.AppServices` — DI
259
263
  registration is by interface via assembly scan, so yours and the generated one cannot coexist; keep
260
264
  only yours (there is no attribute to suppress generation of the handler alone — drop the whole
261
265
  route with `CrudMapOptions.Exclude` if you also need to change the request shape).
@@ -281,12 +285,12 @@ constructor.
281
285
 
282
286
  Every body shares `title: "Error"`, `status`, `type` (the status's name, e.g. `"Conflict"`), and
283
287
  `traceId` (the current `Activity` id, falling back to `TraceIdentifier`). One registration answers
284
- all three failure kinds — `Minimal.Api/Configs/FluentValidationConfig.cs`'s single
288
+ all three failure kinds — `<YourApp>.Api/Configs/FluentValidationConfig.cs`'s single
285
289
  `AddErrorResponses(...)` call — there is no second place to configure this.
286
290
 
287
291
  ## Architecture rules enforced by tests
288
292
 
289
- `Minimal.App.Tests/Architecture/AppServiceTests.cs` and `RecordArchitectureTests.cs` pin, exactly:
293
+ `<YourApp>.App.Tests/Architecture/AppServiceTests.cs` and `RecordArchitectureTests.cs` pin, exactly:
290
294
 
291
295
  - Every class implementing `IRequestHandler<>`/`IRequestHandler<,>`/`IConsumer<>` must be
292
296
  non-public and `sealed` (`AllHandlerClassesShouldBeInternalAndSealed`).
@@ -298,13 +302,14 @@ all three failure kinds — `Minimal.Api/Configs/FluentValidationConfig.cs`'s si
298
302
  - A `[GenerateDto]` type must expose no property whose type (or generic argument) inherits
299
303
  `DomainEntity` (`DtosWithGenerateDtoAttribute_ShouldNotHaveProperties_ThatAreDomainEntities`) —
300
304
  the DTO boundary must not leak an entity.
301
- - No record type in `Minimal.Domains` or `Minimal.AppServices` may declare a public property with a
305
+ - No record type in `<YourApp>.Domains` or `<YourApp>.AppServices` may declare a public property with a
302
306
  **private** setter (`RecordTypes_ShouldNotContain_PrivateSetters_OnPublicProperties`) — AutoMapper
303
307
  (Mapster's `MapToConstructor`/property assignment) cannot fill it.
304
308
 
305
309
  ## Step-by-step
306
310
 
307
- **mode=manual** (mirror `ManualSample/PurchaseOrder`):
311
+ **mode=manual** (mirror `ManualSample/PurchaseOrder`) — only after confirming the operation is one
312
+ of the five the generator genuinely cannot express (`dknet-feature-lifecycle` §1):
308
313
 
309
314
  1. Add `<Feature>/V1/Actions/Create.cs`: request implementing `IWitResponse<TDto>` with
310
315
  `[FromClaim(ClaimTypes.Name)] ByUser`, a co-located validator, a handler constructing the
@@ -316,6 +321,7 @@ all three failure kinds — `Minimal.Api/Configs/FluentValidationConfig.cs`'s si
316
321
  `Result.Ok()`.
317
322
  4. Wire each into `<Feature>V1Endpoint.cs` with a literal `Map*` call (`dknet-endpoint`) so
318
323
  DataAnnotations validation, if any, is actually enforced.
324
+ 5. Keep `ApiEndpoints/<YourApp>.Client` in step by adding the matching client methods (`I{Plural}Client`, contracts under `Contracts/`) in the same change.
319
325
 
320
326
  **mode=auto** (mirror `AutomatedSample/Product`):
321
327
 
@@ -327,6 +333,7 @@ all three failure kinds — `Minimal.Api/Configs/FluentValidationConfig.cs`'s si
327
333
  4. For an operation that must write more than one aggregate, or must reject a repeat call as a
328
334
  domain failure rather than a `200`, write a `...Command` (not `...Request`) and drop the
329
335
  generated route for that member with `CrudMapOptions.Exclude("MethodName")` in the endpoint.
336
+ 5. Keep `ApiEndpoints/<YourApp>.Client` in step by adding the matching client methods for every route the endpoint maps, in the same change.
330
337
 
331
338
  ## Validation checklist
332
339
 
@@ -386,10 +393,17 @@ the entity to already carry `[CrudCreate]`/`[CrudUpdate]`/`[RaisesEvent]`, and t
386
393
  generate without them. If `mode=` was not supplied, detect it: grep the entity for `[CrudCreate]`;
387
394
  present it means `auto`, absent means `manual`. Say which you detected.
388
395
 
389
- Pick one per aggregate, don't mix them for the same entity:
396
+ Pick one per aggregate, don't mix them for the same entity, and **start from the declarative
397
+ shape**:
390
398
 
391
- - **Hand-written** (below) — every request/validator/handler/spec/DTO is a file you write. Needed whenever the aggregate has a business rule beyond DataAnnotations, a filtered query, idempotent writes, or a DTO that must hide fields.
392
- - **Declarative CRUD generation** (further down) — `[CrudCreate]`/`[CrudUpdate]`/`[GenerateDto]` on the entity itself; `DKNet.SlimBus.Generators` produces the request/handler/route types for you. Only for genuinely plain CRUD — read the validation-gap caveat before choosing it.
399
+ - **Declarative CRUD generation (`mode=auto`, the default)** — `[CrudCreate]`/`[CrudUpdate]`/
400
+ `[CrudAction]` + `[GenerateDto]` on the entity itself; `DKNet.SlimBus.Generators` produces the
401
+ request/handler/route types for you. A business rule that must *refuse* the operation is written
402
+ as a FluentValidation validator against the generated request and still runs, so it is not a
403
+ reason to leave this path.
404
+ - **Hand-written (`mode=manual`)** — every request/validator/handler/spec/DTO is a file you write.
405
+ Step down to it only for idempotent create, an enforced DataAnnotations rule, a multi-aggregate
406
+ transaction, a bespoke query, or `[FromClaim]` acting-user attribution.
393
407
 
394
408
  The `dknet-feature-lifecycle` skill §1 is the authoritative comparison between the two; use it to decide.
395
409
 
@@ -405,7 +419,7 @@ The `dknet-feature-lifecycle` skill §1 is the decision procedure if the mode is
405
419
  #### Required reading
406
420
 
407
421
  1. The reference sections above
408
- 2. `ApiEndpoints/Minimal.AppServices/ManualSample/V1/` (exemplar: `Actions/{Create,Update,Cancel,Delete}.cs`, `Specs/SpecGetPurchaseOrder.cs`, `Queries/{GetPurchaseOrderById,ListPurchaseOrders}.cs`, `Events/`, `PurchaseOrderDto.cs`)
422
+ 2. `ApiEndpoints/<YourApp>.AppServices/ManualSample/V1/` (exemplar: `Actions/{Create,Update,Cancel,Delete}.cs`, `Specs/SpecGetPurchaseOrder.cs`, `Queries/{GetPurchaseOrderById,ListPurchaseOrders}.cs`, `Events/`, `PurchaseOrderDto.cs`)
409
423
 
410
424
  #### Steps
411
425
 
@@ -430,7 +444,7 @@ The `dknet-feature-lifecycle` skill §1 is the decision procedure if the mode is
430
444
 
431
445
  ### Path 2: Declarative CRUD generation (`mode=auto`)
432
446
 
433
- For a genuinely plain CRUD entity, declare the CRUD surface on the entity instead of writing it. Exemplar: `Product` (`ApiEndpoints/Minimal.Domains/Features/AutomatedSample/Entities/Product.cs`, `ApiEndpoints/Minimal.AppServices/AutomatedSample/V1/ProductDto.cs`).
447
+ The default path: declare the CRUD surface on the entity instead of writing it. Exemplar: `Product` (`ApiEndpoints/<YourApp>.Domains/Features/AutomatedSample/Entities/Product.cs`, `ApiEndpoints/<YourApp>.AppServices/AutomatedSample/V1/ProductDto.cs`).
434
448
 
435
449
  Most of the entity-side attributes are `/dknet-entity mode=auto`'s job. This command's own output at
436
450
  the AppServices layer is small on purpose: **one `[GenerateDto]` line, plus any hand-written event
@@ -460,7 +474,7 @@ consumer.** If you find yourself writing a request, validator, or handler here,
460
474
 
461
475
  #### What gets generated
462
476
 
463
- `DKNet.SlimBus.Generators` produces (namespace `Minimal.AppServices.Crud`, not committed — inspect via `dotnet build` then `obj/Generated/DKNet.SlimBus.Generators/`):
477
+ `DKNet.SlimBus.Generators` produces (namespace `<YourApp>.AppServices.Crud`, not committed — inspect via `dotnet build` then `obj/Generated/DKNet.SlimBus.Generators/`):
464
478
 
465
479
  - `Create<Entity>Request` / `Change<Member><Entity>Request` (named after the `[CrudUpdate]` method, e.g. `ChangePriceProductRequest`) + matching `internal sealed` handlers (`Create<Entity>Handler` / `Change<Member><Entity>Handler`) — no hand-written request/validator/handler exists for these.
466
480
  - `<Entity>CrudEndpointExtensions.Map<Entity>Crud()` — GetById/GetList/Delete map straight to `DKNet.AspCore.Extensions`'s generic `MapGetById<TEntity,TKey,TDto>`/`MapGetList`/`MapDeleteById`; Create/Update use the generated handlers above.
@@ -469,7 +483,7 @@ consumer.** If you find yourself writing a request, validator, or handler here,
469
483
 
470
484
  - Do NOT hand-write a request or handler for a `[CrudCreate]`/`[CrudUpdate]` member while its route is still generated — that defeats the point of the generator. A **validator** is the exception and the supported shape: a FluentValidation validator for the generated request runs on the generated route through the group filter. Exclude a route by name (`CrudMapOptions.Exclude(string)`) and hand-write it below the `Map<Entity>Crud(...)` call only when the operation writes more than one aggregate in one transaction. Generated and hand-written routes coexisting in one endpoint is the shipped shape, not a smell — see `ProductV1Endpoint`.
471
485
  - **Validation gap, confirmed live**: a `[Range]`/`[Required]` on a `[CrudCreate]`/`[CrudUpdate]` parameter *is* forwarded onto the generated request property, but it is **never enforced** under this template's endpoint-registration convention — the .NET 10 validation source generator only recognizes literal `Map*(string, Delegate)` calls, and the generated route goes through `DKNet.AspCore.Extensions`'s generic `MapPost<TRequest,TDto>` wrapper instead. `POST /v1/products` with a negative price returns `201`, not `400`. Do not present a DataAnnotations attribute on a generated request as enforced without checking the endpoint's mapping style.
472
- - Acting-user attribution cannot use `[FromClaim]` on a generated request (the generator forwards only `System.ComponentModel.DataAnnotations` attributes) — it goes through `DKNet.EfCore.DataAuthorization`'s `DataOwnerHook` instead, wired once in `Minimal.Api/Configs/ServiceConfigs.cs`, not per-entity.
486
+ - Acting-user attribution cannot use `[FromClaim]` on a generated request (the generator forwards only `System.ComponentModel.DataAnnotations` attributes) — it goes through `DKNet.EfCore.DataAuthorization`'s `DataOwnerHook` instead, wired once in `<YourApp>.Api/Configs/ServiceConfigs.cs`, not per-entity.
473
487
  - No idempotency key support on the generated create route — see `/dknet-endpoint`'s "Alternative: generated CRUD route" section if the feature needs it.
474
488
 
475
489
  #### Steps
@@ -24,7 +24,7 @@ An aggregate is a transactional consistency boundary: everything inside it is sa
24
24
 
25
25
  - If two pieces of data must always be consistent with each other *at the moment they're saved* (e.g. a `PurchaseOrder`'s `Amount` and its `Status` — cancelling and re-pricing an order in the same save must not leave those two fields disagreeing), they belong in the same aggregate. That's why `PurchaseOrder` owns both `Amount` and `Status` directly rather than splitting them into two persisted types.
26
26
  - If two pieces of data can be consistent *eventually*, a moment apart, they belong in separate aggregates, coordinated through a domain event — not a direct object reference.
27
- - Aggregates reference each other by ID (`Guid`), never by object reference. `PurchaseOrder` (`ApiEndpoints/Minimal.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`) does not hold a `Customer` object — it holds a plain `CustomerName` string, no navigation property back to another aggregate.
27
+ - Aggregates reference each other by ID (`Guid`), never by object reference. `PurchaseOrder` (`ApiEndpoints/<YourApp>.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`) does not hold a `Customer` object — it holds a plain `CustomerName` string, no navigation property back to another aggregate.
28
28
 
29
29
  Keep aggregates small. A large aggregate means more contention (every mutation locks the whole thing) and usually signals a boundary was drawn around "things that seem related" rather than "things that must be consistent together."
30
30
 
@@ -41,26 +41,73 @@ An invariant is a rule that must always hold true for an entity (e.g. "amount is
41
41
 
42
42
  - Properties are `{ get; private set; }`. Nothing outside the entity can put it into an invalid state directly.
43
43
  - The constructor establishes the invariant for a new entity. Named mutation methods (`ChangeAmount`, `Cancel` — see `PurchaseOrder` in `dknet-entity`) re-establish it for every mutation, and are the *only* path to changing mutable state.
44
- - A rule that depends on the entity's **own current state** belongs on the entity, or right next to the fetch in the handler when it must read the stored row first. `PurchaseOrder.Cancel(string userId)` is the clearest example: `CancelPurchaseOrderCommandHandler` (`ApiEndpoints/Minimal.AppServices/ManualSample/V1/Actions/Cancel.cs`) checks `order.Status == PurchaseOrderStatus.Cancelled` and fails the request *before* calling `Cancel`; the transition itself still lives on the entity. Note the weakness this leaves — the guard is one call away from being bypassed by a second caller who skips it, so treat handler-side guards as a boundary check, not as the invariant's home.
44
+ - A rule that depends on the entity's **own current state** belongs on the entity, or right next to the fetch in the handler when it must read the stored row first. `PurchaseOrder.Cancel(string userId)` is the clearest example: `CancelPurchaseOrderCommandHandler` (`ApiEndpoints/<YourApp>.AppServices/ManualSample/V1/Actions/Cancel.cs`) checks `order.Status == PurchaseOrderStatus.Cancelled` and fails the request *before* calling `Cancel`; the transition itself still lives on the entity. Note the weakness this leaves — the guard is one call away from being bypassed by a second caller who skips it, so treat handler-side guards as a boundary check, not as the invariant's home.
45
45
  - If a rule needs data external to the entity (e.g. "customer name must be unique across all orders"), that's not an entity invariant — it's a cross-entity business rule, and it belongs in the command handler as a duplicate-check `Specification` query (see `dknet-crud`), because the entity has no way to see other entities.
46
46
 
47
47
  ## When to Use a Domain Event
48
48
 
49
- This codebase has two equally valid ways to raise the same kind of event — pick the one that matches how much control you need over the raise:
50
-
51
- - **Hand-raised** (`PurchaseOrder`): the constructor calls `AddEvent(new PurchaseOrderCreatedEvent(Id, CustomerName, Amount))` directly, in application code you can step through in a debugger. You write the payload by hand. Use this when the event's payload, timing, or "did this actually happen" condition needs logic more specific than "a tracked property changed."
52
- - **Declared** (`Product`): the class carries `[RaisesEvent(EventOperations.Created, Include = [nameof(Id), nameof(Name), nameof(Price)])]` and `[RaisesEvent(EventOperations.Updated, nameof(Price))]` — no line of application code calls `AddEvent` anywhere in `AutomatedSample/`. DKNet's EF Core save hook reads these declarations and raises the composed event records (`ProductCreatedEvent`, `ProductPriceUpdatedEvent`) after a successful `SaveChanges`, driven by the change tracker. Use this when the event is a straightforward "this property changed" notification and you're already using `[CrudCreate]`/`[CrudUpdate]` for the entity.
53
- The trade-offs: you cannot single-step from "constructor ran" to "event raised" the way you can with `AddEvent`, and the payload's name and shape follow a fixed composition rule (`<Entity><NarrowingProps><Operation>Event`) rather than one you choose — verify the composed name against the compiled assembly before wiring a consumer to it.
54
-
55
- Both styles are delivered identically afterward: `ApiEndpoints/Minimal.Infra/Services/EventPublisher.cs` forwards to `IMessageBus` regardless of which raised the event.
56
-
57
- Reach for either one when **something outside this aggregate might care that this happened** — another aggregate needs to react, or an external system needs to be notified. `Product`'s declared `Created` event is consumed both in-process and over Azure Service Bus (`ProductCreatedNotificationHandler`), exactly as a hand-raised one would be.
49
+ First decide **whether** there should be an event at all. Reach for one when **something outside
50
+ this aggregate might care that this happened** — another aggregate needs to react, or an external
51
+ system needs to be notified. `Product`'s declared `Created` event is consumed both in-process and
52
+ over Azure Service Bus (`ProductCreatedNotificationHandler`).
58
53
 
59
54
  Do NOT reach for an event when the effect is entirely local to this one request:
60
- - Setting a computed field during the same handler → just do it in the handler or the entity method, no event needed.
55
+
56
+ - Setting a computed field during the same handler → just do it in the handler or the entity method.
61
57
  - A validation failure → return `Result.Fail(...)`, don't publish an event.
62
58
 
63
- If you can't name a concrete future subscriber (even a logging handler counts, but "just in case" doesn't), it's not an event yet — add it when a real consumer appears.
59
+ If you can't name a concrete future subscriber (even a logging handler counts, but "just in case"
60
+ doesn't), it's not an event yet — add it when a real consumer appears.
61
+
62
+ ### How to raise it — take the highest rung that works
63
+
64
+ | Rung | Form | Use it when |
65
+ |---|---|---|
66
+ | 1 | **`[RaisesEvent]` on the class** | The event means "this entity was created" or "these properties changed". Default. |
67
+ | 2 | **`AddEvent<TEvent>()` in a mutation method** | Rung 1 can't decide *whether* to raise — the condition is real logic — but the payload is still a projection of the entity. |
68
+ | 3 | **`AddEvent(new TEvent(...))`** | The payload is not a projection of the entity: it carries a computed value, a before/after pair, or data the entity doesn't hold. |
69
+
70
+ **Rung 1 — declared (`Product`).** The class carries
71
+ `[RaisesEvent(EventOperations.Created, Include = [nameof(Id), nameof(Name), nameof(Price)])]` and
72
+ `[RaisesEvent(EventOperations.Updated, nameof(Price))]`; no line of application code calls
73
+ `AddEvent` anywhere in `AutomatedSample/`. DKNet's EF Core save hook reads the declarations and
74
+ raises the composed records (`ProductCreatedEvent`, `ProductPriceUpdatedEvent`) after a successful
75
+ `SaveChanges`, driven by the change tracker. `<YourApp>.App.Tests/Architecture/SampleInvariantTests`
76
+ fails the build if a hand-written `AddEvent(` appears under `AutomatedSample`.
77
+
78
+ What you give up: you cannot single-step from "constructor ran" to "event raised", and the payload's
79
+ name and shape follow a fixed composition rule (`<Entity><NarrowingProps><Operation>Event`) rather
80
+ than one you choose — verify the composed name against the compiled assembly before wiring a
81
+ consumer to it, it has no source file.
82
+
83
+ **Rung 2 — type-only (`AddEvent<TEvent>()`).** Registers the event *type*; the publisher maps the
84
+ entity onto it (`IMapper.Map(entity, entityType, eventType)`) when the save succeeds:
85
+
86
+ ```csharp
87
+ public void Cancel(string byUser)
88
+ {
89
+ if (Status == PurchaseOrderStatus.Cancelled) return; // the raise IS conditional
90
+ Status = PurchaseOrderStatus.Cancelled;
91
+ SetUpdatedBy(byUser);
92
+ AddEvent<PurchaseOrderCancelledEvent>(); // payload projected from the entity
93
+ }
94
+ ```
95
+
96
+ You keep control of *when*, and still hand-write no payload. It **requires an `IMapper`
97
+ registration** — with none, the publisher throws `EventException` rather than silently dropping the
98
+ event — and the event record's members must be mappable from the entity's own properties.
99
+
100
+ **Rung 3 — instance (`AddEvent(new …)`, `PurchaseOrder`).** The constructor calls
101
+ `AddEvent(new PurchaseOrderCreatedEvent(Id, CustomerName, Amount))` directly. Full control of
102
+ payload, name and timing, at the cost of a hand-written record and a hand-written construction site
103
+ that can drift from the entity. Take it only when the payload is genuinely not a projection of the
104
+ entity's current state.
105
+
106
+ All three are delivered identically afterward:
107
+ `ApiEndpoints/<YourApp>.Infra/Services/EventPublisher.cs` forwards to `IMessageBus` regardless of
108
+ which raised the event. Mixing rungs on one entity is allowed — a declared `Created` alongside one
109
+ hand-raised transition event is fine — except under `AutomatedSample`, where the architecture test
110
+ pins it to rung 1 only.
64
111
 
65
112
  ## Avoiding Anemic Domain Models
66
113
 
@@ -77,6 +124,8 @@ The handler's job is orchestration: fetch the entity (via `IRepositorySpec` + a
77
124
  - [ ] Does this type ever get looked up independently of its parent? If no, it's a value object, not an entity.
78
125
  - [ ] Does this rule only need data already on the entity? If yes, enforce it in the entity's constructor/`Update` method, not the handler.
79
126
  - [ ] Can I name a real, current subscriber for this event? If no, skip the event for now.
127
+ - [ ] If it is an event: can `[RaisesEvent]` express it? If not, can `AddEvent<TEvent>()`? Only then
128
+ hand-build the payload with `AddEvent(new …)`.
80
129
  - [ ] Is the handler computing business logic, or just orchestrating fetch → mutate → persist → map? If it's computing, move the logic onto the entity.
81
130
 
82
131
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dknet-docs
3
- description: Generate structured technical documentation and Mermaid architecture diagrams for completed features. Use this when documenting implemented features with README, architecture diagrams, and API references. Invoke as `/dknet-docs <Feature>` to scaffold it for a feature.
3
+ description: Generate structured technical documentation for a completed feature and every endpoint it exposes — README, architecture and flow diagrams drawn with archify (Mermaid as the fallback), per-route API reference, data model, and domain events. Use this when documenting an implemented feature. Invoke as `/dknet-docs <Feature>` to scaffold it for a feature.
4
4
  metadata:
5
5
  kind: workflow
6
6
  arguments: "<Feature>"
@@ -29,7 +29,9 @@ Usage: `/dknet-docs <Feature>`
29
29
  | `data-model.md` | Entity diagram, properties, constraints, relationships |
30
30
  | `events.md` | Domain events catalog with publishers and subscribers |
31
31
 
32
- **Diagram tool**: All diagrams use **Mermaid.js** — rendered natively in GitHub, VS Code Preview, and most wikis. No extra tools required.
32
+ **Diagram tool**: **archify first, Mermaid as the fallback** — see *Diagrams* below. Every endpoint the
33
+ feature exposes gets its own section in `api-reference.md`; a feature is not documented until every
34
+ route it publishes is.
33
35
 
34
36
  **Real examples already in this repo**: this skill is about documenting a *new* feature you just
35
37
  built, not about the two worked samples that ship with the template — but those samples
@@ -107,7 +109,7 @@ X-Idempotency-Key: 6e6f4d3c-1b7e-4c7a-9f1d-8a2b5c6d7e01
107
109
 
108
110
  ### Step 3: Write architecture.md (Diagrams + Data Flow)
109
111
 
110
- **What you're doing**: Show how the feature is structured across layers with a vertical slice diagram. Use Mermaid for all diagrams.
112
+ **What you're doing**: Show how the feature is structured across layers with a vertical slice diagram. Render it with archify per *Diagrams* below; the Mermaid examples here are the fallback shape.
111
113
 
112
114
  **Five diagrams to include**:
113
115
 
@@ -124,13 +126,13 @@ Example Vertical Slice Overview diagram (from the `PurchaseOrder` sample):
124
126
  ```mermaid
125
127
  graph TD
126
128
  Client["Client / Browser"]
127
- subgraph API["Minimal.Api"]
129
+ subgraph API["<YourApp>.Api"]
128
130
  EP["PurchaseOrderV1Endpoint.cs"]
129
131
  end
130
- subgraph AppServices["Minimal.AppServices"]
132
+ subgraph AppServices["<YourApp>.AppServices"]
131
133
  HDL["Command Handlers"]
132
134
  end
133
- subgraph Domains["Minimal.Domains"]
135
+ subgraph Domains["<YourApp>.Domains"]
134
136
  ENT["PurchaseOrder (AggregateRoot)"]
135
137
  end
136
138
  DB[("PostgreSQL")]
@@ -143,6 +145,24 @@ graph TD
143
145
 
144
146
  **What you're doing**: Full endpoint documentation with curl examples, request/response schemas, and error codes.
145
147
 
148
+ **One section per route, no exceptions.** Enumerate the feature's routes from the endpoint config —
149
+ for a generated slice that means every route `Map<Entity>Crud()` publishes (`GET {id}`, `GET /`,
150
+ `POST /`, one `PUT {id}` per `[CrudUpdate]`, `DELETE {id}`, and one route per `[CrudAction]`), minus
151
+ anything `CrudMapOptions.Exclude` drops, plus every hand-mapped route below it. Build once and read
152
+ the generated `<Entity>CrudEndpoints.g.cs` rather than guessing the list. Each section states:
153
+
154
+ | Line | What it must say |
155
+ |---|---|
156
+ | Method + path | Including the `/v{n}` version segment when `EnableVersioning` is on |
157
+ | Authorization | The scope the route requires, and whether it comes from `[EndpointGroupScope]`, `o.Configure(...)` or a hand-mapped `.RequireAuthorization(...)` — or "none, `RequireAuthorization` off" |
158
+ | Idempotency | `X-Idempotency-Key` required, or explicitly "not idempotent — a retry creates a second row" for a generated create route |
159
+ | Request | Every bound field, its type, whether it is required, and where it binds from (body, route, query, claim) |
160
+ | Response | Status code + DTO, and every error status the route can actually return |
161
+ | Enforcement | Whether the route's DataAnnotations are enforced (literal `Map*` call) or forwarded only (generated/generic route) |
162
+ | curl | One runnable example including required headers |
163
+
164
+ Add a `sequence` diagram for any route whose flow is not "bind → validate → handler → save → map".
165
+
146
166
  Copy `templates/api-reference-template.md` from this skill's folder and fill it in — Endpoints Summary table, one section per endpoint (query params/request body, response, error table, curl example), Common Error Response Format. Note the pagination-defaults gotcha: a hand-written list query's `pageIndex`/`pageSize` defaults differ from the generated `MapGetList` route's contract (`pageNumber`/`pageSize` default 1/1000, configurable ceiling via `DKNet:ListQuery`, plus `fromDate`/`toDate` windowing) — document whichever contract this feature's route actually uses. Also document the standard error format: `result.Response()` (`DKNet.AspCore.Extensions.Responses`) converts a failed `FluentResults` result into `ProblemDetails` with messages under an `errors` array; a `NotFoundError` produces the same shape with `status: 404`.
147
167
 
148
168
  Example endpoint entry (from the `PurchaseOrder` sample):
@@ -214,31 +234,43 @@ public sealed record PurchaseOrderCreatedEvent(Guid Id, string CustomerName, dec
214
234
 
215
235
  ---
216
236
 
217
- ## Mermaid Diagram Types Reference
237
+ ## Diagrams — archify first, Mermaid as the fallback
218
238
 
219
- Use appropriate Mermaid diagram types for different aspects:
239
+ Draw architecture, flow, sequence, data-flow and lifecycle diagrams with **archify**
240
+ (<https://github.com/tt-a1i/archify>), a skill that turns a small typed JSON spec into a validated,
241
+ self-contained HTML diagram plus SVG/PNG exports. Install it once with
242
+ `npx skills add tt-a1i/archify`, then invoke the `archify` skill and let it author, validate and
243
+ deliver each diagram — do not hand-write its JSON from memory.
220
244
 
221
- | Diagram type | Mermaid keyword | When to use |
222
- |-------------|-----------------|-------------|
223
- | Component flow | `graph TD` / `graph LR` | Overview of layers, event flows |
224
- | Request sequence | `sequenceDiagram` | How a specific API call flows step-by-step |
225
- | Entity classes | `classDiagram` | Class relationships and properties |
226
- | Entity-Relation | `erDiagram` | Database table structure |
227
- | State machine | `stateDiagram-v2` | Status transitions |
228
- | Timeline | `timeline` | Feature evolution, release history |
245
+ Pick the archify type per diagram:
229
246
 
230
- **All Mermaid diagrams are fenced code blocks**:
247
+ | Diagram | archify type | Mermaid fallback |
248
+ |---|---|---|
249
+ | Vertical slice across the six projects | `architecture` | `graph TD` |
250
+ | Request lifecycle for one route (endpoint → handler → entity → DB → response) | `sequence` | `sequenceDiagram` |
251
+ | Domain-event path (raise → publisher → in-memory + external consumers) | `dataflow` | `graph LR` |
252
+ | Status transitions the handlers actually perform | `lifecycle` | `stateDiagram-v2` |
253
+ | A multi-step business process or approval gate | `workflow` | `graph TD` |
254
+ | Table structure | *(none — keep Mermaid)* | `erDiagram` |
255
+
256
+ Commit both the spec and the export next to the docs that reference them:
231
257
 
232
- ````md
233
- ```mermaid
234
- graph TD
235
- A --> B
236
258
  ```
237
- ````
259
+ docs/features/<feature>/diagrams/
260
+ ├── <feature>-slice.architecture.json ← archify source, re-render from this
261
+ ├── <feature>-slice.svg ← exported, referenced from architecture.md
262
+ ├── <feature>-create.sequence.json
263
+ └── <feature>-create.svg
264
+ ```
238
265
 
239
- They render automatically on GitHub, GitLab, VS Code (Markdown Preview), Docusaurus, and most modern wikis.
266
+ Reference an export from markdown with a plain image link
267
+ (`![Vertical slice](diagrams/<feature>-slice.svg)`), so it renders on GitHub, in VS Code preview and
268
+ in a wiki without archify installed.
240
269
 
241
- ---
270
+ **Fallback rule:** if archify is not installed and you cannot install it, write the diagram as a
271
+ fenced ```mermaid block in the markdown itself and say in your report that the diagram is Mermaid
272
+ pending an archify render. Never ship a feature doc with no diagram at all. An `erDiagram` stays
273
+ Mermaid either way — archify has no table-schema type.
242
274
 
243
275
  ## Feature Docs Folder Structure
244
276
 
@@ -248,9 +280,10 @@ docs/
248
280
  └── purchase-orders/ ← kebab-case folder name
249
281
  ├── README.md ← Overview (START HERE)
250
282
  ├── architecture.md ← Diagrams + vertical slice
251
- ├── api-reference.md ← Endpoints + examples + curl
283
+ ├── api-reference.md ← One section per route + examples + curl
252
284
  ├── data-model.md ← Entity diagram + constraints
253
285
  ├── events.md ← Domain events + subscribers
286
+ ├── diagrams/ ← archify sources + exported SVGs
254
287
  └── decisions/ ← Optional ADRs
255
288
  └── adr-001-idempotency-key-strategy.md
256
289
  ```
@@ -281,16 +314,25 @@ You are producing authoritative feature documentation for a vertical slice that
281
314
  - how the acting user is attributed (`[FromClaim]` vs `DataOwnerHook`).
282
315
  For automated slices, read event names off the compiled assembly, not off a guess at the
283
316
  composition rule.
284
- 2. Render the four required artifacts under `docs/features/<feature>/` (or `docs/<feature>/` if the slice is template-internal):
317
+ 2. Enumerate every route the feature publishes (generated + hand-mapped) before writing anything —
318
+ this is the spine of `api-reference.md` and the completeness check at the end.
319
+ 3. Render the required artifacts under `docs/features/<feature>/` (or `docs/<feature>/` if the slice is template-internal):
285
320
  - `README.md` (feature overview + quick links)
286
- - `architecture.md` (Mermaid diagrams: layer flow, sequence for Create, ER snippet)
287
- - `data-model.md`
288
- - `api-reference.md`
289
- 3. Cross-link from `docs/features/README.md` (or whichever index file lists features).
290
- 4. Verify all referenced files exist and Mermaid blocks render (no stray fences).
321
+ - `architecture.md` (vertical slice, create sequence, event path — drawn with archify per
322
+ *Diagrams*, Mermaid only if archify is unavailable)
323
+ - `data-model.md` (ER diagram stays Mermaid)
324
+ - `api-reference.md` (one section per route, per the table in Step 4)
325
+ - `events.md` when the feature raises or consumes any event
326
+ - `diagrams/` holding each archify JSON source next to its exported SVG
327
+ 4. Cross-link from `docs/features/README.md` (or whichever index file lists features).
328
+ 5. Verify every route has a section, every referenced diagram file exists, and any Mermaid block
329
+ renders (no stray fences).
291
330
 
292
331
  ### Constraints
293
332
 
294
333
  - Do not invent fields, validators, events, or endpoints — only document what's in the code.
295
334
  - Use the templates in the skill folder verbatim where they fit; deviations need a one-line note.
296
335
  - No hand-wavey language ("flexible", "robust", "scalable") — describe what the code actually does.
336
+ - Every route the feature publishes has its own `api-reference.md` section. A missing route is a
337
+ failed run, not a trim.
338
+ - Diagrams are archify renders unless archify could not be installed; say which in the report.
@@ -51,14 +51,14 @@ Authorization: Bearer {token}
51
51
 
52
52
  | Layer | Path |
53
53
  |-------|------|
54
- | Domain Entity | `ApiEndpoints/Minimal.Domains/Features/{EntityFolder}/Entities/{EntityName}.cs` |
55
- | EF Core Mapper | `ApiEndpoints/Minimal.Infra/Features/{EntityFolder}/Mappers/{EntityName}Mapper.cs` |
56
- | Create Handler | `ApiEndpoints/Minimal.AppServices/{FeatureFolder}/V1/Actions/Create.cs` |
57
- | Update Handler | `ApiEndpoints/Minimal.AppServices/{FeatureFolder}/V1/Actions/Update.cs` |
58
- | Delete Handler | `ApiEndpoints/Minimal.AppServices/{FeatureFolder}/V1/Actions/Delete.cs` |
59
- | Domain Events | `ApiEndpoints/Minimal.AppServices/{FeatureFolder}/V1/Events/` |
60
- | Query Specs | `ApiEndpoints/Minimal.AppServices/{FeatureFolder}/V1/Specs/` |
61
- | API Endpoints | `ApiEndpoints/Minimal.Api/ApiEndpoints/{EntityName}V1Endpoints.cs` |
54
+ | Domain Entity | `ApiEndpoints/<YourApp>.Domains/Features/{EntityFolder}/Entities/{EntityName}.cs` |
55
+ | EF Core Mapper | `ApiEndpoints/<YourApp>.Infra/Features/{EntityFolder}/Mappers/{EntityName}Mapper.cs` |
56
+ | Create Handler | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Actions/Create.cs` |
57
+ | Update Handler | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Actions/Update.cs` |
58
+ | Delete Handler | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Actions/Delete.cs` |
59
+ | Domain Events | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Events/` |
60
+ | Query Specs | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Specs/` |
61
+ | API Endpoints | `ApiEndpoints/<YourApp>.Api/ApiEndpoints/{EntityName}V1Endpoints.cs` |
62
62
 
63
63
  ## Related Documentation
64
64
 
@@ -1,5 +1,12 @@
1
1
  # {FeatureName} — API Reference
2
2
 
3
+ > Every route the feature publishes gets its own section below — generated CRUD routes included.
4
+ > Read the list off the built `<Entity>CrudEndpoints.g.cs` plus the endpoint config's hand-mapped
5
+ > calls; do not trim one because it looks obvious. Each section states method + path, the scope it
6
+ > requires and where that scope is declared, whether it is idempotent, every bound field and where it
7
+ > binds from, every status it can return, whether its DataAnnotations are enforced, and a runnable
8
+ > curl. See the `dknet-docs` skill, Step 4.
9
+
3
10
  **Base Path**: `/api/v1/{feature-route}`
4
11
  **Auth**: Bearer token required on all endpoints
5
12
  **Content-Type**: `application/json`