@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,66 +1,126 @@
|
|
|
1
|
-
# Data Standard: Redis Cache
|
|
1
|
+
# Data Standard: Redis Cache
|
|
2
2
|
|
|
3
3
|
> **Scope:** data-nosql-cache
|
|
4
4
|
> **Layer:** 2
|
|
5
5
|
> **Keywords:** redis, cache, distributed-cache, session, rate-limiting, stackexchange-redis, azure-cache
|
|
6
6
|
> **Load When:** caching, session storage, or rate limiting is in scope
|
|
7
7
|
|
|
8
|
-
**Verified against:** .NET 10 + StackExchange.Redis 2.
|
|
8
|
+
**Verified against:** .NET 10 (SDK 10.0.400-preview) + Microsoft.Extensions.Caching.StackExchangeRedis 10.0.12 + StackExchange.Redis 2.13.17 + EF Core 10.0.12. The registration, cache-aside handler, invalidation and rate-limit blocks compile with `TreatWarningsAsErrors` over the `Result` of `result-pattern.md`; package dependencies read from the nuget.org nuspec on 2026-10-03. Last-verified: 2026-10-03.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
|
-
Distributed cache for session data, rate limiting, and frequently-read data.
|
|
13
|
+
Distributed cache for session data, rate limiting, and frequently-read data -- a Redis on the Coolify VPS or a managed one (Azure Cache for Redis, Upstash); the code is the same. Redis is never the primary store: the `ApplicationDbContext` is.
|
|
14
14
|
|
|
15
15
|
## Setup
|
|
16
16
|
```xml
|
|
17
|
-
<PackageReference Include="
|
|
18
|
-
|
|
17
|
+
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.12" />
|
|
18
|
+
<!-- only when you inject IConnectionMultiplexer directly (rate limiting, pub/sub) -->
|
|
19
|
+
<PackageReference Include="StackExchange.Redis" Version="2.13.17" />
|
|
19
20
|
```
|
|
20
21
|
|
|
22
|
+
`Microsoft.Extensions.Caching.StackExchangeRedis` 10.0.12 depends on `StackExchange.Redis` >= 2.7.27, so NuGet resolves that floor unless you pin a newer one. When the code touches `IConnectionMultiplexer`, reference `StackExchange.Redis` explicitly at the latest **2.x**: the 3.x line is a new major that the caching package has not moved to.
|
|
23
|
+
|
|
21
24
|
```csharp
|
|
22
|
-
// Program.cs
|
|
23
|
-
|
|
25
|
+
// Program.cs (or an AddRedisCache extension)
|
|
26
|
+
using Microsoft.Extensions.Caching.StackExchangeRedis;
|
|
27
|
+
using StackExchange.Redis;
|
|
28
|
+
|
|
29
|
+
public static class RedisRegistration
|
|
24
30
|
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
public static IServiceCollection AddRedisCache(this IServiceCollection services, IConfiguration configuration)
|
|
32
|
+
{
|
|
33
|
+
var connectionString = configuration.GetConnectionString("Redis")
|
|
34
|
+
?? throw new InvalidOperationException("ConnectionStrings:Redis is not configured.");
|
|
35
|
+
|
|
36
|
+
services.AddSingleton<IConnectionMultiplexer>(_ => ConnectionMultiplexer.Connect(connectionString));
|
|
37
|
+
services.AddStackExchangeRedisCache(options => options.InstanceName = "myapp:");
|
|
38
|
+
services.AddOptions<RedisCacheOptions>().Configure<IServiceProvider>((options, sp) =>
|
|
39
|
+
options.ConnectionMultiplexerFactory = () => Task.FromResult(sp.GetRequiredService<IConnectionMultiplexer>()));
|
|
40
|
+
return services;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
28
43
|
```
|
|
29
44
|
|
|
45
|
+
- The connection string comes from configuration: user-secrets in development, a Coolify environment variable (`ConnectionStrings__Redis`) in production -- never from code.
|
|
46
|
+
- `InstanceName` prefixes every `IDistributedCache` key (`myapp:`), so two apps on one Redis do not collide.
|
|
47
|
+
- The missing-connection-string `throw` runs once at startup, which is configuration validation, not control flow.
|
|
48
|
+
- One shared `IConnectionMultiplexer` serves both the cache and direct Redis calls (it is thread-safe and expensive to create). It is registered as a **lazy** singleton and handed to the cache through `Configure<IServiceProvider>`, so the first use connects -- never `ConnectionMultiplexer.Connect` during registration: with `AbortOnConnectFail` (the default outside Azure) an unreachable Redis would stop the app from booting, making the cache a hard dependency. Never call `services.BuildServiceProvider()` inside the factory either: that builds a second container with a second multiplexer. If you do not need the multiplexer directly, drop it and set `options.Configuration = connectionString`.
|
|
49
|
+
|
|
30
50
|
## Cache-Aside Pattern
|
|
51
|
+
|
|
52
|
+
Cache-aside lives in the slice handler, next to the `ApplicationDbContext` -- no `CachedProductService` wrapping a repository. Cache the **response DTO**, not the EF entity (navigation properties and change tracking do not serialize cleanly), and return a `Result`: a miss in both cache and database is `NotFound`, never `null`.
|
|
53
|
+
|
|
31
54
|
```csharp
|
|
32
|
-
|
|
55
|
+
// Features/ProductFeature/GetProduct/GetProductHandler.cs
|
|
56
|
+
using System.Text.Json;
|
|
57
|
+
using Microsoft.EntityFrameworkCore;
|
|
58
|
+
using Microsoft.Extensions.Caching.Distributed;
|
|
59
|
+
|
|
60
|
+
public sealed record GetProductRequest(Guid Id);
|
|
61
|
+
public sealed record ProductResponse(Guid Id, string Name, decimal Price);
|
|
62
|
+
|
|
63
|
+
public static class ProductCacheKeys
|
|
64
|
+
{
|
|
65
|
+
public static string Detail(Guid id) => $"product:detail:{id}";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public sealed class GetProductHandler(ApplicationDbContext db, IDistributedCache cache)
|
|
69
|
+
: IHandler<GetProductRequest, Result<ProductResponse>>
|
|
33
70
|
{
|
|
34
|
-
private readonly
|
|
35
|
-
|
|
71
|
+
private static readonly DistributedCacheEntryOptions CacheEntry = new()
|
|
72
|
+
{
|
|
73
|
+
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15),
|
|
74
|
+
SlidingExpiration = TimeSpan.FromMinutes(5)
|
|
75
|
+
};
|
|
36
76
|
|
|
37
|
-
public async Task<
|
|
77
|
+
public async Task<Result<ProductResponse>> HandleAsync(GetProductRequest request, CancellationToken cancellationToken)
|
|
38
78
|
{
|
|
39
|
-
var
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
JsonSerializer.Serialize(product),
|
|
53
|
-
new DistributedCacheEntryOptions
|
|
54
|
-
{
|
|
55
|
-
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15),
|
|
56
|
-
SlidingExpiration = TimeSpan.FromMinutes(5)
|
|
57
|
-
}, ct);
|
|
79
|
+
var key = ProductCacheKeys.Detail(request.Id);
|
|
80
|
+
|
|
81
|
+
var cached = await cache.GetStringAsync(key, cancellationToken);
|
|
82
|
+
if (cached is not null)
|
|
83
|
+
return JsonSerializer.Deserialize<ProductResponse>(cached);
|
|
84
|
+
|
|
85
|
+
var product = await db.Products
|
|
86
|
+
.Where(p => p.Id == request.Id)
|
|
87
|
+
.Select(p => new ProductResponse(p.Id, p.Name, p.Price))
|
|
88
|
+
.FirstOrDefaultAsync(cancellationToken);
|
|
89
|
+
|
|
90
|
+
if (product is null)
|
|
91
|
+
return ProductErrors.NotFound(request.Id);
|
|
58
92
|
|
|
93
|
+
await cache.SetStringAsync(key, JsonSerializer.Serialize(product), CacheEntry, cancellationToken);
|
|
59
94
|
return product;
|
|
60
95
|
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Invalidation
|
|
61
100
|
|
|
62
|
-
|
|
63
|
-
|
|
101
|
+
The handler that writes invalidates the exact key, **after** `SaveChangesAsync`:
|
|
102
|
+
|
|
103
|
+
```csharp
|
|
104
|
+
// Features/ProductFeature/UpdateProductPrice/UpdateProductPriceHandler.cs
|
|
105
|
+
using Microsoft.EntityFrameworkCore;
|
|
106
|
+
using Microsoft.Extensions.Caching.Distributed;
|
|
107
|
+
|
|
108
|
+
public sealed record UpdateProductPriceRequest(Guid Id, decimal Price);
|
|
109
|
+
|
|
110
|
+
public sealed class UpdateProductPriceHandler(ApplicationDbContext db, IDistributedCache cache)
|
|
111
|
+
: IHandler<UpdateProductPriceRequest, Result>
|
|
112
|
+
{
|
|
113
|
+
public async Task<Result> HandleAsync(UpdateProductPriceRequest request, CancellationToken cancellationToken)
|
|
114
|
+
{
|
|
115
|
+
var product = await db.Products.FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken);
|
|
116
|
+
if (product is null)
|
|
117
|
+
return ProductErrors.NotFound(request.Id);
|
|
118
|
+
|
|
119
|
+
product.Price = request.Price;
|
|
120
|
+
await db.SaveChangesAsync(cancellationToken);
|
|
121
|
+
await cache.RemoveAsync(ProductCacheKeys.Detail(request.Id), cancellationToken);
|
|
122
|
+
return Result.Success();
|
|
123
|
+
}
|
|
64
124
|
}
|
|
65
125
|
```
|
|
66
126
|
|
|
@@ -85,22 +145,38 @@ Examples:
|
|
|
85
145
|
auth:ratelimit:user123:login
|
|
86
146
|
```
|
|
87
147
|
|
|
148
|
+
Keep key builders in one static class per feature (`ProductCacheKeys`) so the reader and the invalidator cannot drift.
|
|
149
|
+
|
|
88
150
|
## Rate Limiting Pattern
|
|
151
|
+
|
|
152
|
+
For a single instance, ASP.NET Core's built-in rate limiter (`AddRateLimiter`) is enough. Across several instances the counter must be shared, so it lives in Redis:
|
|
153
|
+
|
|
89
154
|
```csharp
|
|
90
|
-
|
|
155
|
+
// Infrastructure/RateLimiting/RedisRateLimiter.cs
|
|
156
|
+
using StackExchange.Redis;
|
|
157
|
+
|
|
158
|
+
public sealed class RedisRateLimiter(IConnectionMultiplexer redis, TimeProvider timeProvider)
|
|
91
159
|
{
|
|
92
|
-
|
|
93
|
-
|
|
160
|
+
public async Task<bool> IsRateLimitedAsync(string userId, string action, int maxPerMinute)
|
|
161
|
+
{
|
|
162
|
+
var key = $"ratelimit:{action}:{userId}:{timeProvider.GetUtcNow():yyyyMMddHHmm}";
|
|
163
|
+
var db = redis.GetDatabase();
|
|
94
164
|
|
|
95
|
-
|
|
96
|
-
|
|
165
|
+
var count = await db.StringIncrementAsync(key);
|
|
166
|
+
if (count == 1)
|
|
167
|
+
await db.KeyExpireAsync(key, TimeSpan.FromMinutes(1));
|
|
97
168
|
|
|
98
|
-
|
|
169
|
+
return count > maxPerMinute;
|
|
170
|
+
}
|
|
99
171
|
}
|
|
100
172
|
```
|
|
101
173
|
|
|
174
|
+
The minute comes from `TimeProvider` (testable with `FakeTimeProvider`), never `DateTime.UtcNow`.
|
|
175
|
+
|
|
102
176
|
## Anti-Patterns
|
|
103
177
|
- Never cache sensitive data (passwords, tokens, PII) without encryption
|
|
104
|
-
- Never use Redis as primary store
|
|
105
|
-
- Never omit TTL
|
|
106
|
-
- Never
|
|
178
|
+
- Never use Redis as primary store -- it's a cache, not a database
|
|
179
|
+
- Never omit TTL -- unbounded keys fill memory
|
|
180
|
+
- Never wrap the cache in a `XService`/repository class -- cache-aside is three lines in the handler
|
|
181
|
+
- Never return `null` for "not found" -- the handler returns `Result` with `NotFound`
|
|
182
|
+
- Never swallow `RedisConnectionException` in an empty `catch`: a cache outage must be visible (health check + logs). Falling back to the database while Redis is down is a product decision: record it in `decisions.md` and implement it once, at the cache boundary, not per call site
|
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
# Data Standard: Azure Cosmos DB
|
|
1
|
+
# Data Standard: Azure Cosmos DB (optional — outside the baked-in stack)
|
|
2
2
|
|
|
3
3
|
> **Scope:** data-nosql
|
|
4
|
-
> **Layer:**
|
|
4
|
+
> **Layer:** 3 (opcional — nenhuma rule nem persona o carrega; só sob demanda via `morph-spec standards --find`, depois de a decisão existir)
|
|
5
5
|
> **Keywords:** cosmos-db, nosql, document-database, event-store, partition-key, azure-cosmos
|
|
6
|
-
> **Load When:**
|
|
6
|
+
> **Load When:** `decisions.md` already records the choice of Cosmos DB over Neon for a specific store
|
|
7
7
|
|
|
8
|
-
**Verified against:** .NET 10 + Microsoft.Azure.Cosmos
|
|
8
|
+
**Verified against:** .NET 10 (SDK 10.0.400-preview) + Microsoft.Azure.Cosmos 3.63.2 + Newtonsoft.Json 13.0.4 — every `csharp` block below compiles with `TreatWarningsAsErrors` against the `Result`/`Error` types of `backend-dotnet-result-pattern`. Last-verified: 2026-10-03.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
|
-
NoSQL document database — use for high-throughput, globally distributed, or schema-flexible data.
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
✅
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
**Cosmos DB is not part of the stack.** The default store is Neon (Postgres) through EF Core, with the `ApplicationDbContext` used directly in the slice. Reach for Cosmos only when a requirement Postgres cannot meet is written down in `decisions.md` (the "why not Neon" included). Everything below applies the stack conventions to that exception: the client is injected straight into the slice handler, ids are `Guid.CreateVersion7()`, expected failures are `Result` values, and classes are `sealed`.
|
|
15
|
+
|
|
16
|
+
## When It Can Be Justified
|
|
17
|
+
|
|
18
|
+
✅ Multi-region writes are a hard requirement
|
|
19
|
+
✅ Append-only event stream with write throughput beyond what the Neon plan sustains
|
|
20
|
+
✅ Documents whose schema varies per tenant and is never joined
|
|
21
|
+
❌ Relational data, joins, reporting — use Neon (Postgres) via EF Core
|
|
22
|
+
❌ Small datasets — cost-inefficient, and one more store to operate
|
|
23
|
+
❌ "Flexible schema" alone — Postgres `jsonb` already covers it
|
|
22
24
|
|
|
23
25
|
## Container Design
|
|
26
|
+
|
|
24
27
|
```json
|
|
25
28
|
{
|
|
26
29
|
"partitionKey": "/tenantId",
|
|
@@ -37,88 +40,128 @@ NoSQL document database — use for high-throughput, globally distributed, or sc
|
|
|
37
40
|
### Partition Key Strategy
|
|
38
41
|
- High cardinality: userId, tenantId, orderId
|
|
39
42
|
- Even distribution: avoid hot partitions (e.g., /status = "active" for 90% of docs)
|
|
40
|
-
- Query alignment: partition key
|
|
43
|
+
- Query alignment: the partition key appears in most queries — a cross-partition query fans out to every partition and is billed for each
|
|
41
44
|
|
|
42
45
|
## SDK Setup
|
|
46
|
+
|
|
47
|
+
Packages: `Microsoft.Azure.Cosmos` **and** an explicit `Newtonsoft.Json` reference — the Cosmos 3.x build targets fail the build without it, even when the client serializes with System.Text.Json.
|
|
48
|
+
|
|
49
|
+
One `CosmosClient` per process (it owns the connection pool), and the `Container` registered as a keyed singleton so a slice asks for exactly the container it uses. No repository wrapper: `Container` already is the data-access API, the same way `DbSet<T>` is under EF Core.
|
|
50
|
+
|
|
43
51
|
```csharp
|
|
44
|
-
|
|
45
|
-
builder.
|
|
46
|
-
new
|
|
47
|
-
|
|
48
|
-
new
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
builder.Services.AddSingleton<ICosmosRepository<Order>>(sp =>
|
|
58
|
-
new CosmosRepository<Order>(
|
|
59
|
-
sp.GetRequiredService<CosmosClient>(),
|
|
60
|
-
config["CosmosDb:DatabaseId"],
|
|
61
|
-
"orders"));
|
|
52
|
+
builder.Services.AddSingleton(_ => new CosmosClient(
|
|
53
|
+
builder.Configuration["CosmosDb:ConnectionString"],
|
|
54
|
+
new CosmosClientOptions
|
|
55
|
+
{
|
|
56
|
+
UseSystemTextJsonSerializerWithOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web),
|
|
57
|
+
ApplicationName = "MyApp"
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
builder.Services.AddKeyedSingleton("order-events", (sp, _) =>
|
|
61
|
+
sp.GetRequiredService<CosmosClient>().GetContainer(
|
|
62
|
+
builder.Configuration["CosmosDb:DatabaseId"], "order-events"));
|
|
62
63
|
```
|
|
63
64
|
|
|
64
|
-
##
|
|
65
|
+
## Document Shape
|
|
66
|
+
|
|
65
67
|
```csharp
|
|
66
|
-
public
|
|
68
|
+
public sealed record OrderEvent(
|
|
69
|
+
string Id,
|
|
70
|
+
string TenantId,
|
|
71
|
+
string Type,
|
|
72
|
+
DateTimeOffset CreatedAt,
|
|
73
|
+
int? Ttl = null);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`JsonSerializerDefaults.Web` serializes `Id` as `id`, which is the property Cosmos requires.
|
|
77
|
+
|
|
78
|
+
## Handler in the Slice
|
|
79
|
+
|
|
80
|
+
The stream APIs return the status code instead of throwing, so a missing item or a duplicate id is an expected outcome mapped to a `Result` — not a `try/catch` around `CosmosException`. Unexpected failures still surface as exceptions and reach the global exception handler.
|
|
81
|
+
|
|
82
|
+
```csharp
|
|
83
|
+
public sealed record AppendOrderEvent(string TenantId, string Type);
|
|
84
|
+
|
|
85
|
+
public static class OrderEventErrors
|
|
67
86
|
{
|
|
68
|
-
|
|
87
|
+
public static Error NotFound(string id) =>
|
|
88
|
+
Error.NotFound("OrderEvent.NotFound", $"Order event '{id}' was not found.");
|
|
89
|
+
|
|
90
|
+
public static Error Duplicate(string id) =>
|
|
91
|
+
Error.Conflict("OrderEvent.Duplicate", $"Order event '{id}' already exists.");
|
|
69
92
|
|
|
70
|
-
public
|
|
93
|
+
public static Error StoreFailure(HttpStatusCode status) =>
|
|
94
|
+
Error.Unexpected("OrderEvent.StoreFailure", $"Cosmos DB answered {(int)status}.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public sealed class AppendOrderEventHandler(
|
|
98
|
+
[FromKeyedServices("order-events")] Container container,
|
|
99
|
+
TimeProvider clock)
|
|
100
|
+
{
|
|
101
|
+
public async Task<Result<OrderEvent>> HandleAsync(AppendOrderEvent command, CancellationToken ct)
|
|
71
102
|
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
var item = new OrderEvent(
|
|
104
|
+
Guid.CreateVersion7().ToString(),
|
|
105
|
+
command.TenantId,
|
|
106
|
+
command.Type,
|
|
107
|
+
clock.GetUtcNow());
|
|
108
|
+
|
|
109
|
+
using var payload = new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(item, JsonSerializerOptions.Web));
|
|
110
|
+
using var response = await container.CreateItemStreamAsync(
|
|
111
|
+
payload, new PartitionKey(item.TenantId), cancellationToken: ct);
|
|
112
|
+
|
|
113
|
+
return response.StatusCode switch
|
|
78
114
|
{
|
|
79
|
-
|
|
80
|
-
|
|
115
|
+
HttpStatusCode.Created => item,
|
|
116
|
+
HttpStatusCode.Conflict => OrderEventErrors.Duplicate(item.Id),
|
|
117
|
+
_ => OrderEventErrors.StoreFailure(response.StatusCode)
|
|
118
|
+
};
|
|
81
119
|
}
|
|
120
|
+
}
|
|
82
121
|
|
|
83
|
-
|
|
122
|
+
public sealed class GetOrderEventHandler([FromKeyedServices("order-events")] Container container)
|
|
123
|
+
{
|
|
124
|
+
public async Task<Result<OrderEvent>> HandleAsync(string tenantId, string id, CancellationToken ct)
|
|
84
125
|
{
|
|
85
|
-
var
|
|
86
|
-
|
|
87
|
-
foreach (var (k, v) in parameters) query = query.WithParameter(k, v);
|
|
126
|
+
using var response = await container.ReadItemStreamAsync(
|
|
127
|
+
id, new PartitionKey(tenantId), cancellationToken: ct);
|
|
88
128
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
var page = await feed.ReadNextAsync();
|
|
94
|
-
results.AddRange(page);
|
|
95
|
-
}
|
|
96
|
-
return results;
|
|
97
|
-
}
|
|
129
|
+
if (response.StatusCode == HttpStatusCode.NotFound)
|
|
130
|
+
return OrderEventErrors.NotFound(id);
|
|
131
|
+
if (!response.IsSuccessStatusCode)
|
|
132
|
+
return OrderEventErrors.StoreFailure(response.StatusCode);
|
|
98
133
|
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
var item = await JsonSerializer.DeserializeAsync<OrderEvent>(
|
|
135
|
+
response.Content, JsonSerializerOptions.Web, ct);
|
|
136
|
+
return item is null ? OrderEventErrors.NotFound(id) : item;
|
|
137
|
+
}
|
|
101
138
|
}
|
|
102
139
|
```
|
|
103
140
|
|
|
104
|
-
|
|
105
|
-
```csharp
|
|
106
|
-
public abstract class CosmosDocument
|
|
107
|
-
{
|
|
108
|
-
[JsonPropertyName("id")]
|
|
109
|
-
public string Id { get; set; } = Guid.NewGuid().ToString();
|
|
141
|
+
The endpoint maps the `Result` exactly as any other slice does (`result.Match(...)` with the `ErrorType` → HTTP mapping of `backend-dotnet-result-pattern`).
|
|
110
142
|
|
|
111
|
-
|
|
112
|
-
public abstract string PartitionKey { get; }
|
|
143
|
+
Queries take parameters, never string concatenation, and pin the partition key in `QueryRequestOptions` so the query does not fan out:
|
|
113
144
|
|
|
114
|
-
|
|
115
|
-
|
|
145
|
+
```csharp
|
|
146
|
+
var query = new QueryDefinition("SELECT * FROM c WHERE c.tenantId = @tenantId AND c.type = @type")
|
|
147
|
+
.WithParameter("@tenantId", tenantId)
|
|
148
|
+
.WithParameter("@type", type);
|
|
116
149
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
150
|
+
using var feed = container.GetItemQueryIterator<OrderEvent>(
|
|
151
|
+
query, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(tenantId) });
|
|
120
152
|
```
|
|
121
153
|
|
|
154
|
+
## Anti-Patterns
|
|
155
|
+
|
|
156
|
+
| Do NOT | Do Instead |
|
|
157
|
+
|--------|------------|
|
|
158
|
+
| Pick Cosmos without a `decisions.md` entry | Neon (Postgres) via EF Core is the default store |
|
|
159
|
+
| Wrap `Container` in a generic `CosmosRepository<T>` | Inject the keyed `Container` into the slice handler |
|
|
160
|
+
| `try/catch (CosmosException)` for 404/409 | Stream APIs + `StatusCode` → `Result` |
|
|
161
|
+
| `Guid.NewGuid()` for document ids | `Guid.CreateVersion7()` |
|
|
162
|
+
| `DateTime.UtcNow` in the handler | `TimeProvider` (testable with `FakeTimeProvider`) |
|
|
163
|
+
| A `CosmosClient` per request | One singleton client per process |
|
|
164
|
+
|
|
122
165
|
## Cost Optimization
|
|
123
166
|
- Enable TTL on transient data (session, temp tokens)
|
|
124
167
|
- Use serverless mode for dev/test environments
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CSS Animation Patterns
|
|
2
2
|
|
|
3
3
|
> **Scope:** universal
|
|
4
|
-
> **Layer:** 0 (
|
|
4
|
+
> **Layer:** 0 (resumo injetado pela rule `frontend-standards` ao editar `**/*.tsx`/`**/*.css`/`**/*.scss`; texto completo sob demanda via `morph-spec standards --find`)
|
|
5
5
|
> **Keywords:** css, animation, keyframe, transition, stagger, shimmer
|
|
6
6
|
> **Load When:** always
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CSS Naming Convention
|
|
2
2
|
|
|
3
3
|
> **Scope:** universal
|
|
4
|
-
> **Layer:** 0 (
|
|
4
|
+
> **Layer:** 0 (resumo injetado pela rule `frontend-standards` ao editar `**/*.tsx`/`**/*.css`/`**/*.scss`; texto completo sob demanda via `morph-spec standards --find`)
|
|
5
5
|
> **Keywords:** css, bem, naming, class names, conventions
|
|
6
6
|
> **Load When:** always
|
|
7
7
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# Motion Patterns — Motion for React
|
|
1
|
+
# Motion Patterns — Motion for React (formerly Framer Motion)
|
|
2
2
|
|
|
3
3
|
> **Scope:** frontend-nextjs-ui
|
|
4
4
|
> **Layer:** 3
|
|
5
5
|
> **Keywords:** motion, framer-motion, animation, transition, variants, gesture, scroll
|
|
6
6
|
> **Load When:** nextjs-expert active
|
|
7
7
|
|
|
8
|
-
**Verified against:** Motion for React 12 (`motion`, imports from `motion/react`). Last-verified: 2026-
|
|
8
|
+
**Verified against:** Motion for React 12+ (`motion`, imports from `motion/react`); the npm registry and the `motion`/`framer-motion`/`motion-dom` 14.0.0 typings were read on 2026-10-03 (`motion/react` re-exports `framer-motion`, which re-exports `motion-dom`: `Variants`, `useInView`, `AnimatePresence`, `useReducedMotion`, `staggerChildren`/`delayChildren` all present). The TSX snippets are documentary. Last-verified: 2026-10-03.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
Reference guide for implementing animations and transitions in Next.js projects using **Motion for React
|
|
12
|
+
Reference guide for implementing animations and transitions in Next.js projects using **Motion for React** -- the library the stack table calls "Framer Motion" (its former name). Install the `motion` package and import from `motion/react`. `framer-motion` is still published in lockstep (`motion@14` depends on `framer-motion@14` and re-exports it), so both import paths reach the same code; the rule is one path per codebase, and it is `motion/react`. Covers the canonical variants library, design token bridge, scroll and gesture patterns, loading states, and mandatory WCAG accessibility compliance.
|
|
13
13
|
|
|
14
14
|
**Scope, and where the other motion stack starts.** This standard governs component-level motion: enter and exit, gestures, mount and unmount transitions, short scroll reveals in an app or product UI. When the scroll position itself drives the scene (frame scrubs, pinned sequences, parallax, a cinematic landing page), that is GSAP plus ScrollTrigger plus Lenis: see `frontend/scroll-driven/smooth-scroll.md` and `frontend/gsap/gsap-core.md`. The two stacks may share a page but never the same element, because both write inline styles and will fight for control. On a page that runs Lenis, use the `useReveal` recipe in `frontend/scroll-driven/scroll-components.md` rather than `whileInView`, so a single library owns the answer to when an element is visible.
|
|
15
15
|
|
|
@@ -80,11 +80,11 @@ export const pageTransition: Variants = {
|
|
|
80
80
|
|
|
81
81
|
## Section 2: Design Token Bridge
|
|
82
82
|
|
|
83
|
-
This file bridges the CSS variables defined in `.morph/context/design-system.md` to JavaScript values consumed by
|
|
83
|
+
This file bridges the CSS variables defined in `.morph/context/design-system.md` to JavaScript values consumed by Motion. Place at `lib/design-tokens.ts`.
|
|
84
84
|
|
|
85
85
|
```typescript
|
|
86
86
|
// lib/design-tokens.ts
|
|
87
|
-
// Maps CSS variables from .morph/context/design-system.md to JS values for
|
|
87
|
+
// Maps CSS variables from .morph/context/design-system.md to JS values for Motion
|
|
88
88
|
export const durations = {
|
|
89
89
|
instant: 0.05,
|
|
90
90
|
fast: 0.15,
|
|
@@ -171,38 +171,54 @@ import { durations, easings } from '@/lib/design-tokens';
|
|
|
171
171
|
|
|
172
172
|
## Section 5: AnimatePresence (Page Transitions)
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
The root `app/layout.tsx` stays a **Server Component** -- marking it `'use client'` turns the whole tree into client code and forbids `metadata` exports there. The client-only part (`AnimatePresence`, `usePathname`) lives in a small provider that the layout renders:
|
|
175
175
|
|
|
176
176
|
```tsx
|
|
177
|
-
//
|
|
177
|
+
// components/page-transition.tsx
|
|
178
178
|
'use client';
|
|
179
179
|
import { AnimatePresence, motion } from 'motion/react';
|
|
180
180
|
import { usePathname } from 'next/navigation';
|
|
181
181
|
import { pageTransition } from '@/motion/variants';
|
|
182
182
|
|
|
183
|
-
export
|
|
183
|
+
export function PageTransition({ children }: { children: React.ReactNode }) {
|
|
184
184
|
const pathname = usePathname();
|
|
185
185
|
|
|
186
186
|
return (
|
|
187
|
-
<
|
|
187
|
+
<AnimatePresence mode="wait">
|
|
188
|
+
<motion.main
|
|
189
|
+
key={pathname}
|
|
190
|
+
variants={pageTransition}
|
|
191
|
+
initial="hidden"
|
|
192
|
+
animate="visible"
|
|
193
|
+
exit="exit"
|
|
194
|
+
>
|
|
195
|
+
{children}
|
|
196
|
+
</motion.main>
|
|
197
|
+
</AnimatePresence>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
// app/layout.tsx — Server Component (no 'use client')
|
|
204
|
+
import type { Metadata } from 'next';
|
|
205
|
+
import { PageTransition } from '@/components/page-transition';
|
|
206
|
+
|
|
207
|
+
export const metadata: Metadata = { title: 'App' };
|
|
208
|
+
|
|
209
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
210
|
+
return (
|
|
211
|
+
<html lang="pt-BR">
|
|
188
212
|
<body>
|
|
189
|
-
<
|
|
190
|
-
<motion.main
|
|
191
|
-
key={pathname}
|
|
192
|
-
variants={pageTransition}
|
|
193
|
-
initial="hidden"
|
|
194
|
-
animate="visible"
|
|
195
|
-
exit="exit"
|
|
196
|
-
>
|
|
197
|
-
{children}
|
|
198
|
-
</motion.main>
|
|
199
|
-
</AnimatePresence>
|
|
213
|
+
<PageTransition>{children}</PageTransition>
|
|
200
214
|
</body>
|
|
201
215
|
</html>
|
|
202
216
|
);
|
|
203
217
|
}
|
|
204
218
|
```
|
|
205
219
|
|
|
220
|
+
`children` passed from the server layout into the client provider stay Server Components; only `PageTransition` ships to the browser.
|
|
221
|
+
|
|
206
222
|
- `mode="wait"` ensures the exit animation completes before the entering page renders.
|
|
207
223
|
- The `key` prop must change on navigation — use `usePathname()` from `next/navigation`.
|
|
208
224
|
- Keep page transitions subtle (`y: 8`, fast duration) — aggressive transitions hurt perceived performance.
|
|
@@ -211,7 +227,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|
|
211
227
|
|
|
212
228
|
## Section 6: Loading Patterns
|
|
213
229
|
|
|
214
|
-
Prefer CSS `animate-pulse` for skeleton loaders over
|
|
230
|
+
Prefer CSS `animate-pulse` for skeleton loaders over Motion — CSS animations run off the main thread and have lower performance overhead.
|
|
215
231
|
|
|
216
232
|
```tsx
|
|
217
233
|
// components/skeleton.tsx
|
|
@@ -242,7 +258,7 @@ function SkeletonCard() {
|
|
|
242
258
|
|
|
243
259
|
- Use design token CSS variables (`--radius-md`, `--color-neutral-200`, `--space-*`) — never hardcode dimensions or colors.
|
|
244
260
|
- Skeleton shapes should mirror the actual content layout to minimize layout shift on load.
|
|
245
|
-
- For spinner/progress indicators that need JS control, use a simple
|
|
261
|
+
- For spinner/progress indicators that need JS control, use a simple Motion `animate={{ rotate: 360 }}` with `repeat: Infinity`.
|
|
246
262
|
|
|
247
263
|
---
|
|
248
264
|
|
|
@@ -288,7 +304,7 @@ When `shouldReduceMotion` is `true`:
|
|
|
288
304
|
}
|
|
289
305
|
```
|
|
290
306
|
|
|
291
|
-
This CSS fallback catches animations outside
|
|
307
|
+
This CSS fallback catches animations outside Motion (Tailwind `animate-*`, third-party components, CSS keyframes).
|
|
292
308
|
|
|
293
309
|
---
|
|
294
310
|
|
|
@@ -303,7 +319,7 @@ This CSS fallback catches animations outside Framer Motion (Tailwind `animate-*`
|
|
|
303
319
|
| Forgetting `animation-fill-mode: backwards` in stagger | Will cause a flash before the animation starts |
|
|
304
320
|
| `AnimatePresence` without `key` changes | Exit animations will never fire |
|
|
305
321
|
| Using `motion` for skeleton loaders | Use CSS `animate-pulse` — lower performance cost |
|
|
306
|
-
| `npm install framer-motion` / `import ... from 'framer-motion'` | `npm install motion` / `import ... from 'motion/react'` —
|
|
322
|
+
| `npm install framer-motion` / `import ... from 'framer-motion'` | `npm install motion` / `import ... from 'motion/react'` — same code underneath, but one import path per codebase |
|
|
307
323
|
| Need ScrollTrigger, SVG morph, or complex timelines | Use GSAP — see `frontend/gsap/gsap-core.md` |
|
|
308
324
|
|
|
309
325
|
---
|
|
@@ -314,12 +330,12 @@ This CSS fallback catches animations outside Framer Motion (Tailwind `animate-*`
|
|
|
314
330
|
npm install motion
|
|
315
331
|
```
|
|
316
332
|
|
|
317
|
-
> The package is `motion` and components import from `motion/react`.
|
|
318
|
-
>
|
|
319
|
-
>
|
|
320
|
-
>
|
|
333
|
+
> The package is `motion` and components import from `motion/react`. `framer-motion`
|
|
334
|
+
> is not deprecated: it is published with the same version and `motion` depends on
|
|
335
|
+
> it. New code still depends on `motion` and imports from `motion/react`, so the
|
|
336
|
+
> codebase has a single import path; do not add `framer-motion` alongside it.
|
|
321
337
|
|
|
322
|
-
Verify the installed version supports the features used here (requires motion >= 11):
|
|
338
|
+
Verify the installed version supports the features used here (requires motion >= 11; current major on npm is 14):
|
|
323
339
|
|
|
324
340
|
```bash
|
|
325
341
|
npm list motion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Next.js Naming Conventions Standard
|
|
2
2
|
|
|
3
3
|
> **Scope:** frontend/nextjs/naming-conventions
|
|
4
|
-
> **Layer:** 1 (
|
|
4
|
+
> **Layer:** 1 (resumo injetado pela rule `nextjs-standards` ao editar `**/*.ts`/`**/*.tsx`; texto completo sob demanda via `morph-spec standards --find`)
|
|
5
5
|
> **Keywords:** naming, conventions, file names, component names, hooks, typescript
|
|
6
6
|
> **Load When:** creating any new file in a Next.js project
|
|
7
7
|
|