@polymorphism-tech/morph-spec 8.45.0 → 8.47.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 (146) hide show
  1. package/CLAUDE.md +6 -5
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +2 -4
  4. package/bin/validate.js +4 -25
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -2
  9. package/framework/MORPH.md +9 -9
  10. package/framework/commands/create-umbraco-lp.md +2 -0
  11. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +1 -1
  14. package/framework/commands/morph-infra.md +17 -203
  15. package/framework/commands/morph-media.md +3 -0
  16. package/framework/commands/morph-preflight.md +5 -28
  17. package/framework/commands/morph-proposal.md +2 -2
  18. package/framework/commands/morph-troubleshoot.md +2 -14
  19. package/framework/docs/CLI.md +8 -7
  20. package/framework/hooks/claude-code/core/state-sync.js +13 -1
  21. package/framework/hooks/shared/e2e-check.js +3 -4
  22. package/framework/hooks/shared/state-writer.js +1 -0
  23. package/framework/rules/ai-agents-standards.md +18 -16
  24. package/framework/rules/asaas-standards.md +3 -1
  25. package/framework/rules/csharp-standards.md +10 -8
  26. package/framework/rules/data-standards.md +5 -5
  27. package/framework/rules/dotnet-templates.md +1 -1
  28. package/framework/rules/frontend-standards.md +4 -2
  29. package/framework/rules/gsap-standards.md +10 -5
  30. package/framework/rules/hangfire-standards.md +3 -1
  31. package/framework/rules/infrastructure-standards.md +4 -2
  32. package/framework/rules/morph-workflow.md +2 -2
  33. package/framework/rules/neon-auth-standards.md +3 -1
  34. package/framework/rules/neon-standards.md +6 -4
  35. package/framework/rules/nextjs-standards.md +13 -11
  36. package/framework/rules/nextjs-templates.md +1 -1
  37. package/framework/rules/resend-standards.md +3 -1
  38. package/framework/schemas/config.schema.json +4 -0
  39. package/framework/schemas/feature.schema.json +4 -4
  40. package/framework/skills/README.md +1 -1
  41. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  42. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  43. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  44. package/framework/skills/core/morph-brainstorming/SKILL.md +3 -3
  45. package/framework/skills/core/morph-hotfix/SKILL.md +4 -4
  46. package/framework/skills/core/morph-implement/SKILL.md +3 -3
  47. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  48. package/framework/skills/core/morph-media/SKILL.md +3 -3
  49. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  50. package/framework/skills/core/morph-proposal/SKILL.md +3 -3
  51. package/framework/skills/core/morph-review/SKILL.md +30 -17
  52. package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
  53. package/framework/skills/core/morph-standards/SKILL.md +6 -6
  54. package/framework/skills/core/morph-uiux/SKILL.md +3 -3
  55. package/framework/standards/STANDARDS.json +59 -56
  56. package/framework/standards/ai-agents/evals-with-cache.md +1 -1
  57. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  58. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  59. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  60. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  61. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  62. package/framework/standards/ai-agents/setup.md +1 -1
  63. package/framework/standards/ai-agents/structured-output.md +1 -1
  64. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  65. package/framework/standards/architecture/code-comments.md +2 -2
  66. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  67. package/framework/standards/backend/api/validation.md +135 -47
  68. package/framework/standards/backend/database/postgresql/database.md +11 -7
  69. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  70. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  71. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  72. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  73. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  74. package/framework/standards/backend/dotnet/testing.md +1 -1
  75. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  76. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  77. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  78. package/framework/standards/data/nosql/cache/redis.md +120 -44
  79. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  80. package/framework/standards/frontend/design-system/animations.md +1 -1
  81. package/framework/standards/frontend/design-system/naming.md +1 -1
  82. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  83. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  84. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  85. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  86. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  87. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  88. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  89. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  90. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  91. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  92. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  93. package/framework/templates/infrastructure/github/README.md +2 -1
  94. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  95. package/package.json +1 -1
  96. package/src/commands/observability/telemetry.js +3 -5
  97. package/src/commands/project/init.js +2 -1
  98. package/src/commands/project/update.js +16 -18
  99. package/src/commands/state/approve.js +17 -0
  100. package/src/commands/state/gate-check.js +4 -14
  101. package/src/commands/validation/verify.js +99 -57
  102. package/src/core/paths/output-schema.js +1 -1
  103. package/src/dashboard/app/App.jsx +0 -2
  104. package/src/dashboard/app/components/CommandPalette.jsx +2 -3
  105. package/src/dashboard/app/components/EntityLink.jsx +0 -1
  106. package/src/dashboard/app/components/Markdown.jsx +1 -1
  107. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  108. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  109. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  110. package/src/dashboard/scanner/activity-scanner.js +1 -1
  111. package/src/dashboard/scanner/docs-scanner.js +5 -23
  112. package/src/dashboard/scanner/features-scanner.js +1 -1
  113. package/src/dashboard/scanner/index.js +0 -2
  114. package/src/dashboard/scanner/rules-scanner.js +15 -10
  115. package/src/dashboard/scanner/skills-scanner.js +2 -3
  116. package/src/lib/ci/ci-verify.js +118 -0
  117. package/src/lib/ci/github-ci.js +192 -0
  118. package/src/lib/ci/workflow-scaffold.js +122 -0
  119. package/src/lib/evaluator-report.js +13 -0
  120. package/src/lib/risk-detector.js +8 -0
  121. package/src/lib/stack/stack-profile.js +1 -1
  122. package/src/lib/telemetry/verify-event.js +3 -2
  123. package/src/lib/validators/reuse/reuse-validator.js +2 -3
  124. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  125. package/src/lib/validators/validation-runner.js +8 -88
  126. package/src/lib/validators/verify-runner.js +22 -72
  127. package/src/scripts/setup-infra.js +33 -22
  128. package/src/utils/agents-installer.js +20 -8
  129. package/src/utils/file-copier.js +7 -5
  130. package/framework/commands/commit.md +0 -166
  131. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  132. package/framework/rubrics/backend.json +0 -41
  133. package/framework/rubrics/default.json +0 -36
  134. package/framework/rubrics/frontend.json +0 -36
  135. package/framework/templates/project/validate.js +0 -97
  136. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  137. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  138. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  139. package/src/lib/validators/architecture/index.js +0 -7
  140. package/src/lib/validators/css/css-validator.js +0 -340
  141. package/src/lib/validators/css/index.js +0 -7
  142. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  143. package/src/lib/validators/design-system/index.js +0 -7
  144. package/src/lib/validators/score-aggregator.js +0 -244
  145. package/src/lib/validators/ui/index.js +0 -7
  146. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -1,66 +1,126 @@
1
- # Data Standard: Redis Cache (Azure Cache for Redis)
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.x + Microsoft.Extensions.Caching.StackExchangeRedis. Last-verified: 2026-05-20.
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="StackExchange.Redis" Version="2.*" />
18
- <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.*" />
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
- builder.Services.AddStackExchangeRedisCache(opts =>
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
- opts.Configuration = config["Redis:ConnectionString"]; // from Key Vault
26
- opts.InstanceName = "myapp:"; // Prefix to avoid key collisions
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
- public class CachedProductService : IProductService
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 IDistributedCache _cache;
35
- private readonly ApplicationDbContext _db;
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<Product?> GetAsync(Guid id, CancellationToken ct = default)
77
+ public async Task<Result<ProductResponse>> HandleAsync(GetProductRequest request, CancellationToken cancellationToken)
38
78
  {
39
- var cacheKey = $"product:{id}";
40
-
41
- // Try cache first
42
- var cached = await _cache.GetStringAsync(cacheKey, ct);
43
- if (cached != null)
44
- return JsonSerializer.Deserialize<Product>(cached);
45
-
46
- // Cache miss → read from DB
47
- var product = await _db.Products.FindAsync([id], ct);
48
- if (product == null) return null;
49
-
50
- // Cache with TTL
51
- await _cache.SetStringAsync(cacheKey,
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
- public async Task InvalidateAsync(Guid id, CancellationToken ct = default)
63
- => await _cache.RemoveAsync($"product:{id}", ct);
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
- public async Task<bool> IsRateLimitedAsync(string userId, string action, int maxPerMinute)
155
+ // Infrastructure/RateLimiting/RedisRateLimiter.cs
156
+ using StackExchange.Redis;
157
+
158
+ public sealed class RedisRateLimiter(IConnectionMultiplexer redis, TimeProvider timeProvider)
91
159
  {
92
- var key = $"ratelimit:{action}:{userId}:{DateTime.UtcNow:yyyyMMddHHmm}";
93
- var db = _redis.GetDatabase();
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
- var count = await db.StringIncrementAsync(key);
96
- if (count == 1) await db.KeyExpireAsync(key, TimeSpan.FromMinutes(1));
165
+ var count = await db.StringIncrementAsync(key);
166
+ if (count == 1)
167
+ await db.KeyExpireAsync(key, TimeSpan.FromMinutes(1));
97
168
 
98
- return count > maxPerMinute;
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 — it's a cache, not a database
105
- - Never omit TTL — unbounded keys fill memory
106
- - Never catch RedisException silently — degrade gracefully to DB
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:** 2
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:** a NoSQL document store, event store, or globally distributed data is in scope
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 SDK v3. Last-verified: 2026-05-20.
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
- ## When to Use Cosmos DB
16
- ✅ Event store (append-only, high write throughput)
17
- ✅ User activity/session data (variable schema)
18
- ✅ Multi-region writes required
19
- ✅ JSON documents with flexible schema
20
- ❌ Relational data with complex joins (use Azure SQL)
21
- ❌ Small datasets (cost inefficient)
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 should appear in most queries
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
- // Program.cs
45
- builder.Services.AddSingleton(sp =>
46
- new CosmosClient(
47
- config["CosmosDb:ConnectionString"],
48
- new CosmosClientOptions
49
- {
50
- SerializerOptions = new CosmosSerializationOptions
51
- {
52
- PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
53
- },
54
- ApplicationName = "MyApp"
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
- ## Repository Pattern
65
+ ## Document Shape
66
+
65
67
  ```csharp
66
- public class CosmosRepository<T> where T : CosmosDocument
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
- private readonly Container _container;
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 async Task<T?> GetAsync(string id, string partitionKey)
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
- try
73
- {
74
- var response = await _container.ReadItemAsync<T>(id, new PartitionKey(partitionKey));
75
- return response.Resource;
76
- }
77
- catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
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
- return null;
80
- }
115
+ HttpStatusCode.Created => item,
116
+ HttpStatusCode.Conflict => OrderEventErrors.Duplicate(item.Id),
117
+ _ => OrderEventErrors.StoreFailure(response.StatusCode)
118
+ };
81
119
  }
120
+ }
82
121
 
83
- public async Task<IReadOnlyList<T>> QueryAsync(string sql, Dictionary<string, object>? parameters = null)
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 query = new QueryDefinition(sql);
86
- if (parameters != null)
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
- var results = new List<T>();
90
- using var feed = _container.GetItemQueryIterator<T>(query);
91
- while (feed.HasMoreResults)
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
- public async Task UpsertAsync(T document)
100
- => await _container.UpsertItemAsync(document, new PartitionKey(document.PartitionKey));
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
- ## Document Base Class
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
- [JsonPropertyName("_partitionKey")]
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
- [JsonPropertyName("type")]
115
- public string Type => GetType().Name;
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
- [JsonPropertyName("createdAt")]
118
- public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
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 (always load)
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 (always load)
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-05-20.
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**, the library formerly published as `framer-motion`. The package is now `motion` and components import from `motion/react`; the `framer-motion` name only survives inside the Framer design tool. Covers the canonical variants library, design token bridge, scroll and gesture patterns, loading states, and mandatory WCAG accessibility compliance.
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 Framer Motion. Place at `lib/design-tokens.ts`.
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 Framer Motion
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
- Integrate `AnimatePresence` at the layout level to animate between routes:
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
- // app/layout.tsx
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 default function Layout({ children }: { children: React.ReactNode }) {
183
+ export function PageTransition({ children }: { children: React.ReactNode }) {
184
184
  const pathname = usePathname();
185
185
 
186
186
  return (
187
- <html>
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
- <AnimatePresence mode="wait">
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 Framer Motion — CSS animations run off the main thread and have lower performance overhead.
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 Framer Motion `animate={{ rotate: 360 }}` with `repeat: Infinity`.
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 Framer Motion (Tailwind `animate-*`, third-party components, CSS keyframes).
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'` — `framer-motion` is the legacy name |
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`. The old
318
- > `framer-motion` package still resolves as a deprecated alias, but new code
319
- > should depend on `motion` directly. The `framer-motion` import name is only
320
- > kept for projects running inside the Framer design tool.
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 (always)
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