@polymorphism-tech/morph-spec 8.46.0 → 9.0.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.md +53 -44
- package/README.md +3 -4
- package/bin/morph-spec.js +38 -55
- package/bin/validate.js +5 -134
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -2
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/create-umbraco-lp.md +2 -0
- package/framework/commands/migrate-lp-to-umbraco.md +2 -0
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +2 -2
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +17 -203
- package/framework/commands/morph-media.md +3 -0
- package/framework/commands/morph-preflight.md +7 -31
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/commands/morph-troubleshoot.md +2 -14
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +17 -19
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +67 -31
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +3 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/ai-agents-standards.md +18 -16
- package/framework/rules/asaas-standards.md +3 -1
- package/framework/rules/csharp-standards.md +10 -8
- package/framework/rules/data-standards.md +5 -5
- package/framework/rules/dotnet-templates.md +1 -1
- package/framework/rules/frontend-standards.md +4 -2
- package/framework/rules/gsap-standards.md +10 -5
- package/framework/rules/hangfire-standards.md +3 -1
- package/framework/rules/infrastructure-standards.md +4 -2
- package/framework/rules/neon-auth-standards.md +3 -1
- package/framework/rules/neon-standards.md +6 -4
- package/framework/rules/nextjs-standards.md +13 -11
- package/framework/rules/nextjs-templates.md +1 -1
- package/framework/rules/resend-standards.md +3 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -52
- package/framework/skills/README.md +4 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
- package/framework/skills/core/morph-apply/SKILL.md +150 -223
- package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
- package/framework/skills/core/morph-machine/SKILL.md +4 -4
- package/framework/skills/core/morph-media/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/SKILL.md +138 -300
- package/framework/skills/core/morph-review/SKILL.md +112 -137
- package/framework/skills/core/morph-standards/SKILL.md +29 -41
- package/framework/skills/core/morph-uiux/SKILL.md +78 -153
- package/framework/standards/STANDARDS.json +90 -1219
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +2 -3
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
- package/framework/standards/ai-agents/prompt-sources.md +6 -4
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/architecture/code-comments.md +8 -8
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
- package/framework/standards/backend/api/validation.md +135 -47
- package/framework/standards/backend/database/postgresql/database.md +11 -7
- package/framework/standards/backend/database/vector-search-rag.md +207 -324
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
- package/framework/standards/backend/dotnet/result-pattern.md +40 -6
- package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
- package/framework/standards/backend/dotnet/scrutor.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
- package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
- package/framework/standards/data/nosql/cache/redis.md +120 -44
- package/framework/standards/data/nosql/cosmos-db.md +114 -71
- package/framework/standards/frontend/design-system/animations.md +1 -1
- package/framework/standards/frontend/design-system/naming.md +1 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
- package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
- package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
- package/framework/templates/infrastructure/github/README.md +2 -1
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/observability/telemetry.js +3 -5
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +6 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +33 -19
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +95 -138
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +86 -95
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +41 -82
- package/src/core/paths/output-schema.js +20 -67
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/App.jsx +0 -2
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +7 -8
- package/src/dashboard/app/components/EntityLink.jsx +1 -2
- package/src/dashboard/app/components/Markdown.jsx +1 -1
- package/src/dashboard/app/components/Sidebar.jsx +1 -2
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/RulesMap.jsx +3 -3
- package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +6 -16
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +5 -23
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/index.js +0 -2
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/rules-scanner.js +15 -10
- package/src/dashboard/scanner/skills-scanner.js +2 -3
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +134 -135
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-profile.js +1 -1
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +24 -48
- package/src/lib/validators/reuse/symbol-scanner.js +2 -4
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/validation-runner.js +8 -88
- package/src/lib/validators/verify-runner.js +134 -301
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +42 -22
- package/src/utils/agents-installer.js +92 -13
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +34 -6
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/commands/commit.md +0 -166
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rubrics/backend.json +0 -41
- package/framework/rubrics/default.json +0 -36
- package/framework/rubrics/frontend.json +0 -36
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/project/validate.js +0 -97
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/dashboard/app/pages/Rubrics.jsx +0 -97
- package/src/dashboard/scanner/rubrics-scanner.js +0 -12
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -91
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/lib/validators/architecture/architecture-validator.js +0 -270
- package/src/lib/validators/architecture/index.js +0 -7
- package/src/lib/validators/css/css-validator.js +0 -340
- package/src/lib/validators/css/index.js +0 -7
- package/src/lib/validators/design-system/design-system-validator.js +0 -237
- package/src/lib/validators/design-system/index.js +0 -7
- package/src/lib/validators/score-aggregator.js +0 -244
- package/src/lib/validators/ui/index.js +0 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
|
@@ -1,91 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Request Validation — DTO Validation in the Slice
|
|
2
2
|
|
|
3
3
|
> **Scope:** universal
|
|
4
|
-
> **Layer:** 0 (
|
|
4
|
+
> **Layer:** 0 (resumo injetado pela rule `csharp-standards` ao editar `**/*.cs`/`**/*.csproj`; texto completo sob demanda via `morph-spec standards --find`)
|
|
5
5
|
> **Keywords:** validation, fluent, dto, modelstate, fluentvalidation
|
|
6
6
|
> **Load When:** always
|
|
7
7
|
|
|
8
|
-
**Verified against:** .NET 10 (
|
|
8
|
+
**Verified against:** .NET 10 (SDK 10.0.400-preview) + FluentValidation 12.1.1 + EF Core 10.0.12. The validator, `OrderStatus`, `OrderErrors` and state-check handler blocks compile with `TreatWarningsAsErrors` over the `Result`/`Error`/`ValidationError` of `result-pattern.md`. Last-verified: 2026-10-03.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
**MORPH-SPEC Standard** —
|
|
12
|
+
**MORPH-SPEC Standard** — every request DTO is validated in its own slice, before the handler runs, and a failure leaves as a `Result`, never as an exception.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Where each check lives
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
| Check | Lives in | Fails as | HTTP (via `error.ToHttpResult()`) |
|
|
17
|
+
|-------|----------|----------|-----------------------------------|
|
|
18
|
+
| Request **shape**: required, length, range, format, enum, CPF/CNPJ | `{Verb}{Entity}Validator : AbstractValidator<TRequest>` in the slice folder | `ValidationError` (aggregated `Error.Validation` items) from the `ValidationDecorator` | 400 ProblemDetails with `errors[]` |
|
|
19
|
+
| Needs the database: entity exists, unique value | The handler, after loading | `Error.NotFound` / `Error.Conflict` | 404 / 409 |
|
|
20
|
+
| Entity **state** allows the operation (order not completed, not cancelled) | The handler (see *State checks in the handler*) | `Error.Failure` / `Error.Conflict` | 400 / 409 |
|
|
17
21
|
|
|
22
|
+
`ModelState` does not exist in minimal APIs, and DataAnnotations on the request plus FluentValidation would be two validators disagreeing about the same DTO: the stack uses FluentValidation through the decorator, only.
|
|
23
|
+
|
|
24
|
+
## FluentValidation Validator in the Slice
|
|
25
|
+
|
|
26
|
+
```csharp
|
|
27
|
+
// Features/OrderFeature/CreateOrder/CreateOrderValidator.cs
|
|
28
|
+
using FluentValidation;
|
|
29
|
+
|
|
30
|
+
public sealed record CreateOrderItem(Guid ProductId, int Quantity);
|
|
31
|
+
public sealed record CreateOrderRequest(Guid CustomerId, string? CouponCode, IReadOnlyList<CreateOrderItem> Items);
|
|
32
|
+
|
|
33
|
+
public sealed class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
|
|
34
|
+
{
|
|
35
|
+
public CreateOrderValidator()
|
|
36
|
+
{
|
|
37
|
+
RuleFor(r => r.CustomerId).NotEmpty();
|
|
38
|
+
|
|
39
|
+
RuleFor(r => r.CouponCode)
|
|
40
|
+
.MaximumLength(32)
|
|
41
|
+
.Matches("^[A-Z0-9-]+$").WithMessage("Coupon code must be upper-case letters, digits or '-'.")
|
|
42
|
+
.When(r => r.CouponCode is not null);
|
|
43
|
+
|
|
44
|
+
RuleFor(r => r.Items)
|
|
45
|
+
.NotEmpty().WithMessage("An order needs at least one item.");
|
|
46
|
+
|
|
47
|
+
RuleForEach(r => r.Items).ChildRules(item =>
|
|
48
|
+
{
|
|
49
|
+
item.RuleFor(i => i.ProductId).NotEmpty();
|
|
50
|
+
item.RuleFor(i => i.Quantity).InclusiveBetween(1, 100);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
18
54
|
```
|
|
19
|
-
|
|
20
|
-
|
|
55
|
+
|
|
56
|
+
- One validator per request, `sealed`, next to the handler; no validator for a request without input (GetAll).
|
|
57
|
+
- Validate the **shape** only. "Does this customer exist?" needs the database and is the handler's job (a validator with a `DbContext` runs a query per rule and still cannot see a concurrent write).
|
|
58
|
+
- Time-dependent rules take `TimeProvider` and read it inside the rule (`LessThanOrEqualTo(_ => timeProvider.GetUtcNow().Year)`), never `DateTime.UtcNow`.
|
|
59
|
+
|
|
60
|
+
## How a failure reaches HTTP
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
request ─→ ValidationDecorator ─→ any failure? ─yes─→ Result.Failure(new ValidationError([...])) ─→ endpoint: error.ToHttpResult() ─→ 400 + errors[]
|
|
64
|
+
│
|
|
65
|
+
no
|
|
66
|
+
↓
|
|
67
|
+
handler ─→ Result<T>
|
|
21
68
|
```
|
|
22
69
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
70
|
+
The decorator, its registration (`AddValidatorsFromAssembly` before `AddHandlersFromAssembly`) and the `ValidationFailure` → `Error.Validation` conversion are in `backend/dotnet/fluent-validation-vsa.md`; the `ValidationError` record and `ErrorHttpMapping.ToHttpResult()` (which puts the items in the ProblemDetails `errors` extension) are in `backend/dotnet/result-pattern.md`. The endpoint never inspects the error itself.
|
|
71
|
+
|
|
72
|
+
## Validation Patterns
|
|
73
|
+
|
|
74
|
+
| Rule | Why |
|
|
75
|
+
|------|-----|
|
|
76
|
+
| Validate in the slice's validator, never in the endpoint lambda | One place per request; the endpoint only maps HTTP |
|
|
77
|
+
| Return `Result`, never `throw` (`ValidationException`, `ArgumentException`) | Exceptions as flow are forbidden; the decorator already short-circuits with a `ValidationError` |
|
|
78
|
+
| Aggregate all failures | The client fixes every field in one round-trip |
|
|
79
|
+
| Message per rule that the UI can show | `errors[].description` is what the form displays |
|
|
80
|
+
| Shape in the validator, existence/uniqueness/state in the handler | The validator has no reliable view of the database |
|
|
27
81
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
## State checks in the handler
|
|
83
|
+
|
|
84
|
+
Some operations depend on the entity's **state**, not the request's shape. That check lives in the handler, after loading the entity, and returns a `Result`. The rule: validate what is INVALID, not what is valid, so a new flow does not break existing checks.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Original: Created → PendingPayment → Processing → Completed → EmailSent
|
|
88
|
+
Free: Created → Processing → Completed → EmailSent (skips payment)
|
|
31
89
|
```
|
|
32
90
|
|
|
33
|
-
|
|
91
|
+
`if (order.Status != OrderStatus.PendingPayment)` assumes the single original flow and rejects every free order; `if (order.Status >= OrderStatus.Completed)` blocks what is actually invalid and works for both flows.
|
|
92
|
+
|
|
93
|
+
### Enum Design
|
|
34
94
|
|
|
35
95
|
```csharp
|
|
36
96
|
public enum OrderStatus
|
|
37
97
|
{
|
|
38
98
|
Created = 0, PendingPayment = 1, Processing = 2, Completed = 3, EmailSent = 4,
|
|
39
|
-
// Error states (separated range)
|
|
40
99
|
Failed = 100, Cancelled = 101, Refunded = 102
|
|
41
100
|
}
|
|
42
101
|
```
|
|
43
102
|
|
|
44
|
-
|
|
103
|
+
Progress states are ordered; error states sit in a separate range (100+), so `status >= OrderStatus.Completed` covers "finished or failed" in one comparison.
|
|
104
|
+
|
|
105
|
+
### State Check Patterns
|
|
45
106
|
|
|
46
107
|
| Pattern | When | Example |
|
|
47
108
|
|---------|------|---------|
|
|
48
|
-
| Final state check | Block after completion | `if (status >= OrderStatus.Completed)` |
|
|
49
|
-
| Error state list | Block specific errors | `if (
|
|
50
|
-
|
|
|
109
|
+
| Final state check | Block after completion (includes the 100+ error range) | `if (status >= OrderStatus.Completed)` |
|
|
110
|
+
| Error state list | Block specific errors only | `if (status is OrderStatus.Failed or OrderStatus.Cancelled or OrderStatus.Refunded)` |
|
|
111
|
+
| Error range | Block every error state | `if (status >= OrderStatus.Failed)` |
|
|
51
112
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
| Anti-Pattern | Problem | Fix |
|
|
55
|
-
|-------------|---------|-----|
|
|
56
|
-
| `if (status != Expected)` | Assumes single flow | Validate invalid states instead |
|
|
57
|
-
| Exhaustive switch on valid states | Must modify for each new status | Validate only invalid states |
|
|
58
|
-
| Business logic in state validator | Mixed concerns | Separate state validation from business rules |
|
|
59
|
-
|
|
60
|
-
## Complete Example
|
|
113
|
+
### Complete Example
|
|
61
114
|
|
|
62
115
|
```csharp
|
|
63
|
-
|
|
116
|
+
// Features/OrderFeature/OrderErrors.cs
|
|
117
|
+
public static class OrderErrors
|
|
64
118
|
{
|
|
65
|
-
|
|
66
|
-
|
|
119
|
+
public static Error NotFound(Guid orderId) =>
|
|
120
|
+
Error.NotFound("Orders.NotFound", $"The Order with Id '{orderId}' was not found");
|
|
121
|
+
public static readonly Error AlreadyFinished =
|
|
122
|
+
Error.Conflict("Orders.AlreadyFinished", "The order is already completed or in an error state.");
|
|
123
|
+
public static readonly Error PaymentRequired =
|
|
124
|
+
Error.Failure("Orders.PaymentRequired", "The order must be paid before processing.");
|
|
125
|
+
}
|
|
126
|
+
```
|
|
67
127
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (order.Status == OrderStatus.Cancelled) return Result.Failure("Cannot process cancelled order");
|
|
128
|
+
```csharp
|
|
129
|
+
// Features/OrderFeature/ProcessOrder/ProcessOrderHandler.cs
|
|
130
|
+
using Microsoft.EntityFrameworkCore;
|
|
72
131
|
|
|
73
|
-
|
|
74
|
-
if (order.RequiresPayment && !order.IsPaid) return Result.Failure("Payment required");
|
|
132
|
+
public sealed record ProcessOrderRequest(Guid OrderId);
|
|
75
133
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
134
|
+
public sealed class ProcessOrderHandler(ApplicationDbContext db) : IHandler<ProcessOrderRequest, Result>
|
|
135
|
+
{
|
|
136
|
+
public async Task<Result> HandleAsync(ProcessOrderRequest request, CancellationToken cancellationToken)
|
|
137
|
+
{
|
|
138
|
+
var order = await db.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
|
|
139
|
+
if (order is null)
|
|
140
|
+
return OrderErrors.NotFound(request.OrderId);
|
|
141
|
+
|
|
142
|
+
if (order.Status >= OrderStatus.Completed)
|
|
143
|
+
return OrderErrors.AlreadyFinished;
|
|
144
|
+
|
|
145
|
+
if (order.RequiresPayment && !order.IsPaid)
|
|
146
|
+
return OrderErrors.PaymentRequired;
|
|
147
|
+
|
|
148
|
+
order.Status = OrderStatus.Processing;
|
|
149
|
+
await db.SaveChangesAsync(cancellationToken);
|
|
150
|
+
return Result.Success();
|
|
151
|
+
}
|
|
79
152
|
}
|
|
80
153
|
```
|
|
81
154
|
|
|
155
|
+
The state check (`>= Completed`) and the business rule (`RequiresPayment && !IsPaid`) stay separate lines with separate errors: the free flow passes the first and skips the second.
|
|
156
|
+
|
|
157
|
+
## Anti-Patterns
|
|
158
|
+
|
|
159
|
+
| Anti-Pattern | Problem | Fix |
|
|
160
|
+
|-------------|---------|-----|
|
|
161
|
+
| `throw new ValidationException(...)` / `throw new InvalidOperationException(...)` for bad input or state | Exceptions as flow; bypasses the `Result` mapping | Validator → `ValidationError`; handler → typed `Error` |
|
|
162
|
+
| Validation inside the endpoint lambda | Logic leaks out of the slice, not reusable by jobs | `AbstractValidator<TRequest>` in the slice |
|
|
163
|
+
| DataAnnotations + FluentValidation on the same DTO | Two sources of truth | FluentValidation only |
|
|
164
|
+
| `Result.Failure("message")` with a bare string | Does not compile against the `Error` record and has no code/type | Typed `{Entity}Errors` factories |
|
|
165
|
+
| `if (status != Expected)` | Assumes single flow | Validate invalid states instead |
|
|
166
|
+
| Exhaustive switch on valid states | Must modify for each new status | Validate only invalid states |
|
|
167
|
+
| Business rules mixed into the state check | Mixed concerns | Separate state check from business rules |
|
|
168
|
+
|
|
82
169
|
## Checklist
|
|
83
170
|
|
|
84
|
-
- [ ]
|
|
85
|
-
- [ ]
|
|
86
|
-
- [ ]
|
|
87
|
-
- [ ]
|
|
88
|
-
- [ ]
|
|
171
|
+
- [ ] Every request with input has a `sealed` validator in its slice folder?
|
|
172
|
+
- [ ] Validator checks shape only (no `DbContext`)?
|
|
173
|
+
- [ ] Failures come back as `ValidationError` → 400 ProblemDetails with `errors[]`, never an exception?
|
|
174
|
+
- [ ] Existence / uniqueness / state checks in the handler, returning typed `Error`s?
|
|
175
|
+
- [ ] State checks validate INVALID states, with the error range at 100+?
|
|
176
|
+
- [ ] All flows documented in decisions.md and tested?
|
|
89
177
|
|
|
90
178
|
---
|
|
91
179
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **Keywords:** postgresql, postgres, sql, neon, database schema
|
|
6
6
|
> **Load When:** neon or postgresql keywords detected
|
|
7
7
|
|
|
8
|
-
**Verified against:** PostgreSQL 16/17 + EF Core 10 + @neondatabase/serverless. Last-verified: 2026-
|
|
8
|
+
**Verified against:** PostgreSQL 16/17 + EF Core 10.0.12 (Npgsql provider 10.0.3) + @neondatabase/serverless. The EF Core query blocks compile with `TreatWarningsAsErrors` inside a wrapper method over stand-in entities; the SQL is documentary. Last-verified: 2026-10-03.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ Database schema design, migrations, and Row Level Security (RLS) patterns for Ne
|
|
|
21
21
|
|---------|-----------|---------|
|
|
22
22
|
| **Tables** | snake_case, plural | `users`, `user_profiles`, `order_items` |
|
|
23
23
|
| **Columns** | snake_case | `created_at`, `user_id`, `is_active` |
|
|
24
|
-
| **Primary Keys** | `id` | `id uuid primary key
|
|
24
|
+
| **Primary Keys** | `id` | `id uuid primary key` -- value set by the app with `Guid.CreateVersion7()`, no column default |
|
|
25
25
|
| **Foreign Keys** | `{table}_id` | `user_id`, `order_id` |
|
|
26
26
|
| **Timestamps** | `created_at`, `updated_at` | Always include |
|
|
27
27
|
| **Boolean** | `is_` or `has_` prefix | `is_active`, `has_verified_email` |
|
|
@@ -29,9 +29,11 @@ Database schema design, migrations, and Row Level Security (RLS) patterns for Ne
|
|
|
29
29
|
|
|
30
30
|
### Standard Table Template
|
|
31
31
|
|
|
32
|
+
Application ids are generated by the .NET handler with `Guid.CreateVersion7()` (time-ordered, index-friendly), so the column has **no** `default gen_random_uuid()`: one generator per id (the app), and random v4 values would scatter the B-tree index that v7 keeps append-mostly. A row inserted outside the app (seed script, SQL console) passes an explicit id.
|
|
33
|
+
|
|
32
34
|
```sql
|
|
33
35
|
create table public.users (
|
|
34
|
-
id uuid primary key
|
|
36
|
+
id uuid primary key,
|
|
35
37
|
email text unique not null,
|
|
36
38
|
full_name text,
|
|
37
39
|
avatar_url text,
|
|
@@ -135,7 +137,7 @@ dotnet ef migrations script --idempotent
|
|
|
135
137
|
```sql
|
|
136
138
|
-- Create table
|
|
137
139
|
create table if not exists public.example_table (
|
|
138
|
-
id uuid primary key
|
|
140
|
+
id uuid primary key,
|
|
139
141
|
name text not null,
|
|
140
142
|
created_at timestamptz default now() not null
|
|
141
143
|
);
|
|
@@ -164,7 +166,7 @@ create index if not exists idx_example_table_name
|
|
|
164
166
|
|
|
165
167
|
```sql
|
|
166
168
|
create table public.posts (
|
|
167
|
-
id uuid primary key
|
|
169
|
+
id uuid primary key,
|
|
168
170
|
author_id uuid references public.users(id) on delete cascade,
|
|
169
171
|
title text not null,
|
|
170
172
|
content text,
|
|
@@ -274,7 +276,7 @@ var users = await db.Users
|
|
|
274
276
|
### Insert (EF Core)
|
|
275
277
|
|
|
276
278
|
```csharp
|
|
277
|
-
var user = new User { Name = "John", Email = "john@example.com" };
|
|
279
|
+
var user = new User { Id = Guid.CreateVersion7(), Name = "John", Email = "john@example.com" };
|
|
278
280
|
db.Users.Add(user);
|
|
279
281
|
await db.SaveChangesAsync(ct);
|
|
280
282
|
```
|
|
@@ -310,7 +312,9 @@ var posts = await db.Posts
|
|
|
310
312
|
.ToListAsync(ct);
|
|
311
313
|
```
|
|
312
314
|
|
|
313
|
-
### Direct SQL
|
|
315
|
+
### Direct SQL from a Node runtime (@neondatabase/serverless)
|
|
316
|
+
|
|
317
|
+
In the baked stack the Next.js frontend reads data through the .NET API (proxy + TanStack Query), not from the database. This driver is for Node-side tooling (seed scripts, one-off jobs) where there is no .NET API in the path:
|
|
314
318
|
|
|
315
319
|
```typescript
|
|
316
320
|
import { neon } from '@neondatabase/serverless';
|