Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -78,6 +78,8 @@ index plus the relevant [Architecture](../../docs/architecture/) doc.
| [add-permission](add-permission/SKILL.md) | Registering a permission key with the closed action set and scope (Platform / Tenant / Organization). |
| [add-feature-key](add-feature-key/SKILL.md) | Adding a `FeatureKey` / `LimitKey` to the typed registry and wiring entitlement-projection reads. |
| [wire-dapr-pubsub](wire-dapr-pubsub/SKILL.md) | Declaring a Dapr pub/sub topic with the `learnstack.{module}.{aggregate}` convention and `InProcessEventBus` dev fallback. |
| [wire-cross-cutting-foundation](wire-cross-cutting-foundation/SKILL.md) | One-time foundation wiring per backend host — `IExceptionHandler`, 8-step MediatR pipeline, Serilog + OTel, `TenantContextSpanProcessor`, `IErrorTrackingProvider`, `IProviderResilience<TPort>`. Phase 02a deliverable per [ADR-0032](../../docs/decisions/0032-exception-handling-logging-and-observability.md). |
| [add-provider-adapter](add-provider-adapter/SKILL.md) | Adding an external-integration adapter (LiveKit / Stripe / Iyzico / Meilisearch / SeaweedFS / Keycloak / …) with port interface, SDK-exception → `ProviderException` translation, and Polly v8 `ResiliencePipeline` via `IProviderResilience<TPort>`. |

### Backend — guard rules

Expand Down
35 changes: 30 additions & 5 deletions .claude/skills/add-mediatr-handler/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,11 @@ description: >
## Purpose

Write a command/query handler that participates correctly in the LearnStack MediatR
pipeline: validators → audit → idempotency → handler → outbox commit. The pipeline
is shared, so the handler stays focused on its own business logic.
pipeline: `Validation → Logging → Audit → TenantContext → Authorization →
Transaction → OutboxFlush → Handler`. The pipeline is shared (per
[ADR-0032 § Sub-decision 2](../../../docs/decisions/0032-exception-handling-logging-and-observability.md)
and [Standards 02 § Pipeline Behaviors](../../../docs/standards/02-backend-coding.md)),
so the handler stays focused on its own business logic.

## When to use

Expand DownExpand Up@@ -246,10 +249,24 @@ public sealed class EnrollmentsController(ISender mediator) : ControllerBase
## Common pitfalls

- **Throwing for expected failures.** Use `Result.Fail(...)`. Exceptions are for
*unexpected* paths (DB unavailable). The pipeline maps `Result.Fail` to RFC 7807
Problem Details automatically.
*unexpected* paths (DB unavailable, infrastructure faults, programmer error).
`Result.Fail` values are converted to RFC 7807 Problem Details at the
**controller/API boundary** when the endpoint calls
`Result<T>.ToActionResult()` (Step 7 above) — the pipeline itself just
propagates the `Result` unchanged; the explicit `.ToActionResult()` call is
where the mapping happens. Per
[ADR-0032 § Sub-decision 4](../../../docs/decisions/0032-exception-handling-logging-and-observability.md),
`DomainException` is reserved for **bugs** — "expected business-rule
violation" means `Result.Fail(business_rule_violation, …)`, not a throw. The
Roslyn analyzer `LearnStackException-DomainExceptionThrow` flags violations.
- **Throwing `FluentValidation.ValidationException` from a validator.** The
pipeline `ValidationBehavior` already produces
`Result.Fail(validation_failed, errors)`. A throw from the validator is a
bug; FluentValidation runs in collect-mode by default and pipeline behavior
never raises a validation exception.
- **Calling `IAuditStore` directly.** The `AuditLogBehavior` does this for you. A
direct call writes a duplicate row.
direct call writes a duplicate row. The architecture test
`Modules_Do_Not_Write_AuditLog_Directly` enforces it.
- **Two transactions for write + outbox.** The outbox row must be in the **same**
`SaveChangesAsync` as the aggregate. Otherwise the system can publish without
committing (or commit without publishing).
Expand All@@ -260,3 +277,11 @@ public sealed class EnrollmentsController(ISender mediator) : ControllerBase
rejected at runtime because the policy is unknown.
- **Using raw `Guid` in the command.** Loses type safety; the architecture test
`Commands_Use_StronglyTypedIds` rejects it.
- **Logging `ILogger.LogError(ex, ...)` then rethrowing.** The L1
`IExceptionHandler` already logs + records the OTel span error + captures
to `IErrorTrackingProvider` per
[ADR-0032 § Sub-decision 7](../../../docs/decisions/0032-exception-handling-logging-and-observability.md).
Re-logging at the handler doubles the entry.
- **Per-call `Activity.Current?.SetTag("tenant.id", ...)`.** The
`TenantContextSpanProcessor` enriches every span automatically. Per-call
tagging is duplication and a maintenance burden.
Loading