@polymorphism-tech/morph-spec 8.46.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 (140) hide show
  1. package/CLAUDE.md +5 -5
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +1 -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 +1 -1
  9. package/framework/MORPH.md +8 -8
  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 +3 -3
  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/feature.schema.json +1 -3
  39. package/framework/skills/README.md +1 -1
  40. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  41. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  42. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  43. package/framework/skills/core/morph-brainstorming/SKILL.md +3 -3
  44. package/framework/skills/core/morph-hotfix/SKILL.md +4 -4
  45. package/framework/skills/core/morph-implement/SKILL.md +3 -3
  46. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  47. package/framework/skills/core/morph-media/SKILL.md +3 -3
  48. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  49. package/framework/skills/core/morph-proposal/SKILL.md +3 -3
  50. package/framework/skills/core/morph-review/SKILL.md +3 -3
  51. package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
  52. package/framework/skills/core/morph-standards/SKILL.md +6 -6
  53. package/framework/skills/core/morph-uiux/SKILL.md +3 -3
  54. package/framework/standards/STANDARDS.json +59 -56
  55. package/framework/standards/ai-agents/evals-with-cache.md +1 -1
  56. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  57. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  58. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  59. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  60. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  61. package/framework/standards/ai-agents/setup.md +1 -1
  62. package/framework/standards/ai-agents/structured-output.md +1 -1
  63. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  64. package/framework/standards/architecture/code-comments.md +2 -2
  65. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  66. package/framework/standards/backend/api/validation.md +135 -47
  67. package/framework/standards/backend/database/postgresql/database.md +11 -7
  68. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  69. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  70. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  71. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  72. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  73. package/framework/standards/backend/dotnet/testing.md +1 -1
  74. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  75. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  76. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  77. package/framework/standards/data/nosql/cache/redis.md +120 -44
  78. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  79. package/framework/standards/frontend/design-system/animations.md +1 -1
  80. package/framework/standards/frontend/design-system/naming.md +1 -1
  81. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  82. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  83. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  84. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  85. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  86. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  87. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  88. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  89. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  90. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  91. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  92. package/framework/templates/infrastructure/github/README.md +2 -1
  93. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  94. package/package.json +1 -1
  95. package/src/commands/observability/telemetry.js +3 -5
  96. package/src/commands/project/init.js +2 -1
  97. package/src/commands/project/update.js +16 -18
  98. package/src/commands/state/approve.js +17 -0
  99. package/src/commands/state/gate-check.js +4 -14
  100. package/src/commands/validation/verify.js +3 -12
  101. package/src/core/paths/output-schema.js +1 -1
  102. package/src/dashboard/app/App.jsx +0 -2
  103. package/src/dashboard/app/components/CommandPalette.jsx +2 -3
  104. package/src/dashboard/app/components/EntityLink.jsx +0 -1
  105. package/src/dashboard/app/components/Markdown.jsx +1 -1
  106. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  107. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  108. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  109. package/src/dashboard/scanner/activity-scanner.js +1 -1
  110. package/src/dashboard/scanner/docs-scanner.js +5 -23
  111. package/src/dashboard/scanner/features-scanner.js +1 -1
  112. package/src/dashboard/scanner/index.js +0 -2
  113. package/src/dashboard/scanner/rules-scanner.js +15 -10
  114. package/src/dashboard/scanner/skills-scanner.js +2 -3
  115. package/src/lib/evaluator-report.js +13 -0
  116. package/src/lib/risk-detector.js +8 -0
  117. package/src/lib/stack/stack-profile.js +1 -1
  118. package/src/lib/validators/reuse/reuse-validator.js +2 -3
  119. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  120. package/src/lib/validators/validation-runner.js +8 -88
  121. package/src/lib/validators/verify-runner.js +22 -72
  122. package/src/scripts/setup-infra.js +33 -22
  123. package/src/utils/agents-installer.js +20 -8
  124. package/src/utils/file-copier.js +7 -5
  125. package/framework/commands/commit.md +0 -166
  126. package/framework/rubrics/backend.json +0 -41
  127. package/framework/rubrics/default.json +0 -36
  128. package/framework/rubrics/frontend.json +0 -36
  129. package/framework/templates/project/validate.js +0 -97
  130. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  131. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  132. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  133. package/src/lib/validators/architecture/index.js +0 -7
  134. package/src/lib/validators/css/css-validator.js +0 -340
  135. package/src/lib/validators/css/index.js +0 -7
  136. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  137. package/src/lib/validators/design-system/index.js +0 -7
  138. package/src/lib/validators/score-aggregator.js +0 -244
  139. package/src/lib/validators/ui/index.js +0 -7
  140. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** speech to text, transcription, whisper, audio to text, voice transcription
6
6
  > **Read by Claude in:** implement (quando o projeto precisa transcrever áudio para texto)
7
7
 
8
- **Verified against:** OpenAI 2.13.0 + Microsoft.Extensions.AI 10.9.0 (ai-pin 2026-09-08). **Verificação documental**, sem cláusula `provado por` — o ai-kit não transcreve áudio. `AudioClient.TranscribeAudioAsync` e a ponte `AsISpeechToTextClient` foram **medidas por reflexão sobre as assemblies do pin** em 2026-09-08. Last-verified: 2026-09-08.
8
+ **Verified against:** OpenAI 2.13.0 + Microsoft.Extensions.AI 10.9.0 (ai-pin 2026-09-08). **Verificação documental**, sem cláusula `provado por` — o ai-kit não transcreve áudio. `AudioClient.TranscribeAudioAsync` e a ponte `AsISpeechToTextClient` foram **medidas por reflexão sobre as assemblies do pin** em 2026-09-08. Em 2026-10-03 os blocos da API, do `TranscriptionService`, do agente downstream (dentro de um método-invólucro) e do registro DI compilaram com `TreatWarningsAsErrors` contra `OpenAI` 2.13.0 + `Microsoft.Agents.AI` 1.20.0 (o pin de 2026-09-08, que é a data que este campo carrega). Last-verified: 2026-09-08.
9
9
 
10
10
  ---
11
11
 
@@ -32,14 +32,20 @@ No OpenAI SDK (.NET 2.x), o caminho é `AudioClient.TranscribeAudio`:
32
32
  ```csharp
33
33
  using OpenAI.Audio;
34
34
 
35
- AudioClient client = new("whisper-1", apiKey);
36
- AudioTranscription transcription = await client.TranscribeAudioAsync(audioFilePath);
37
- string text = transcription.Text;
35
+ public sealed class TranscribeOnce(AudioClient client)
36
+ {
37
+ public async Task<string> RunAsync(string audioFilePath, CancellationToken ct)
38
+ {
39
+ await using var audio = File.OpenRead(audioFilePath);
40
+ AudioTranscription transcription = await client.TranscribeAudioAsync(audio, Path.GetFileName(audioFilePath), cancellationToken: ct);
41
+ return transcription.Text;
42
+ }
43
+ }
38
44
  ```
39
45
 
40
- O projeto injeta `AudioClient` via DI. Para transcrição com timestamps (ex: sincronização de legenda), use `AudioTranscriptionOptions` com `ResponseFormat = AudioTranscriptionFormat.Verbose`.
46
+ O `AudioClient` chega pelo construtor: ele é registrado **uma vez** no DI com modelo e chave vindos de configuração (ver *Registro DI* abaixo) e nunca é construído inline no chamador. Para transcrição com timestamps (ex: sincronização de legenda), use `AudioTranscriptionOptions` com `ResponseFormat = AudioTranscriptionFormat.Verbose`.
41
47
 
42
- > **Conferência de 2026-09-08.** As sobrecargas existentes na assembly `OpenAI` 2.13.0 são `TranscribeAudioAsync(string audioFilePath, AudioTranscriptionOptions)` e `TranscribeAudioAsync(Stream audio, string audioFilename, AudioTranscriptionOptions, CancellationToken)` — **a de `Stream` exige o nome do arquivo**, porque é dele que o serviço deduz o formato. Passar só o stream não compila.
48
+ > **Conferência de 2026-09-08.** As sobrecargas existentes na assembly `OpenAI` 2.13.0 são `TranscribeAudioAsync(string audioFilePath, AudioTranscriptionOptions)` e `TranscribeAudioAsync(Stream audio, string audioFilename, AudioTranscriptionOptions, CancellationToken)` — **a de `Stream` exige o nome do arquivo**, porque é dele que o serviço deduz o formato. Passar só o stream não compila. **A de caminho não aceita `CancellationToken`** (recompilado em 2026-10-03: o exemplo anterior passava `cancellationToken:` a ela e não compilava, assim como `TimestampGranularities` escrito como coleção) — para honrar o cancelamento do request, abra o arquivo (`File.OpenRead`) e use a de `Stream`, como nos exemplos abaixo.
43
49
  >
44
50
  > **Caminho por abstração (opcional):** `Microsoft.Extensions.AI` 10.9.0 define `ISpeechToTextClient`, e `Microsoft.Extensions.AI.OpenAI` publica `audioClient.AsISpeechToTextClient()`. Vale quando o projeto quer trocar de provedor sem tocar no chamador; o `AudioClient` direto continua sendo o caminho mais curto.
45
51
  >
@@ -49,33 +55,22 @@ O projeto injeta `AudioClient` via DI. Para transcrição com timestamps (ex: si
49
55
 
50
56
  ## Exemplo C# completo
51
57
 
52
- Três entradas: arquivo, bytes (ex: upload do usuário) — as duas devolvem texto simples — e a verbose, com timestamps por palavra e segmento, útil para legendas ou busca por trecho.
58
+ Três entradas: arquivo, bytes (ex: upload do usuário) — as duas devolvem texto simples — e a verbose, com timestamps por palavra e segmento, útil para legendas ou busca por trecho. `TimestampGranularities` é um enum de flags (`AudioTimestampGranularities.Word | ...Segment`), não uma coleção.
53
59
 
54
60
  ```csharp
55
61
  using OpenAI.Audio;
56
- using Microsoft.Extensions.Configuration;
57
62
 
58
- public sealed class TranscriptionService
63
+ public sealed class TranscriptionService(AudioClient client)
59
64
  {
60
- private readonly AudioClient _client;
61
-
62
- public TranscriptionService(IConfiguration config)
63
- {
64
- var model = config["Transcription:Model"] ?? "whisper-1";
65
- var apiKey = config["OpenAI:ApiKey"]!;
66
- _client = new AudioClient(model, apiKey);
67
- }
68
-
69
65
  public async Task<string> TranscribeAsync(string audioFilePath, CancellationToken ct = default)
70
66
  {
71
- AudioTranscription transcription = await _client.TranscribeAudioAsync(audioFilePath, cancellationToken: ct);
72
- return transcription.Text;
67
+ await using var audio = File.OpenRead(audioFilePath);
68
+ return await TranscribeBytesAsync(audio, Path.GetFileName(audioFilePath), ct);
73
69
  }
74
70
 
75
71
  public async Task<string> TranscribeBytesAsync(
76
72
  Stream audioStream,
77
73
  string fileName,
78
- string mediaType = "audio/mpeg",
79
74
  CancellationToken ct = default)
80
75
  {
81
76
  var options = new AudioTranscriptionOptions
@@ -83,7 +78,7 @@ public sealed class TranscriptionService
83
78
  ResponseFormat = AudioTranscriptionFormat.Simple
84
79
  };
85
80
 
86
- AudioTranscription transcription = await _client.TranscribeAudioAsync(
81
+ AudioTranscription transcription = await client.TranscribeAudioAsync(
87
82
  audioStream, fileName, options, cancellationToken: ct);
88
83
  return transcription.Text;
89
84
  }
@@ -93,10 +88,11 @@ public sealed class TranscriptionService
93
88
  var options = new AudioTranscriptionOptions
94
89
  {
95
90
  ResponseFormat = AudioTranscriptionFormat.Verbose,
96
- TimestampGranularities = { AudioTimestampGranularity.Word, AudioTimestampGranularity.Segment }
91
+ TimestampGranularities = AudioTimestampGranularities.Word | AudioTimestampGranularities.Segment
97
92
  };
98
93
 
99
- return await _client.TranscribeAudioAsync(audioFilePath, options, cancellationToken: ct);
94
+ await using var audio = File.OpenRead(audioFilePath);
95
+ return await client.TranscribeAudioAsync(audio, Path.GetFileName(audioFilePath), options, ct);
100
96
  }
101
97
  }
102
98
  ```
@@ -110,12 +106,26 @@ AgentResponse<MeetingActions> actions = await _statusAgent.RunAsync<MeetingActio
110
106
  $"Extraia ações, decisões e responsáveis desta transcrição:\n\n{transcript}");
111
107
  ```
112
108
 
113
- **Registro DI** (`Program.cs`):
109
+ **Registro DI** (`Program.cs`) — o único lugar onde o `AudioClient` é construído:
114
110
 
115
111
  ```csharp
116
- builder.Services.AddSingleton<TranscriptionService>();
112
+ using OpenAI.Audio;
113
+
114
+ public static class TranscriptionRegistration
115
+ {
116
+ public static IServiceCollection AddTranscription(this IServiceCollection services, IConfiguration config)
117
+ {
118
+ services.AddSingleton(_ => new AudioClient(
119
+ config["Transcription:Model"] ?? "whisper-1",
120
+ config["OpenAI:ApiKey"] ?? throw new InvalidOperationException("OpenAI:ApiKey is not configured.")));
121
+ services.AddSingleton<TranscriptionService>();
122
+ return services;
123
+ }
124
+ }
117
125
  ```
118
126
 
127
+ A chave vem de user-secrets em desenvolvimento e de variável de ambiente no deploy (Coolify); o `throw` roda uma vez na resolução do singleton, como validação de configuração. `AudioClient` é thread-safe, então o singleton é seguro.
128
+
119
129
  ---
120
130
 
121
131
  ## Formatos e limites
@@ -81,7 +81,7 @@ Column summary:
81
81
 
82
82
  | Column | Type | Notes |
83
83
  |--------|------|-------|
84
- | `id` | `uuid` | PK, `gen_random_uuid()` |
84
+ | `id` | `uuid` | PK, **no column default** -- set by the app with `Guid.CreateVersion7()` on insert |
85
85
  | `agent_key` | `text NOT NULL` | Logical identifier, e.g. `"proposal-agent"` |
86
86
  | `version` | `int NOT NULL` | Monotonically increasing per `agent_key` |
87
87
  | `content` | `text NOT NULL` | The full system-prompt text |
@@ -238,9 +238,11 @@ UPDATE {{SCHEMA}}.agent_prompts
238
238
  SET is_active = false
239
239
  WHERE agent_key = 'proposal-agent' AND is_active = true;
240
240
 
241
- -- 2. Insert the new version (version = previous max + 1)
242
- INSERT INTO {{SCHEMA}}.agent_prompts (agent_key, version, content, is_active)
241
+ -- 2. Insert the new version (version = previous max + 1);
242
+ -- :'new_id' is a Guid.CreateVersion7() generated by the app/tooling, never gen_random_uuid()
243
+ INSERT INTO {{SCHEMA}}.agent_prompts (id, agent_key, version, content, is_active)
243
244
  VALUES (
245
+ :'new_id',
244
246
  'proposal-agent',
245
247
  (SELECT COALESCE(MAX(version), 0) + 1 FROM {{SCHEMA}}.agent_prompts WHERE agent_key = 'proposal-agent'),
246
248
  'Your new system prompt text here...',
@@ -248,7 +250,7 @@ VALUES (
248
250
  );
249
251
  ```
250
252
 
251
- This keeps a complete, auditable history. Rollback = deactivate the current row and reactivate any previous row.
253
+ This keeps a complete, auditable history. Rollback = deactivate the current row and reactivate any previous row. When the publish runs from the app (a slice handler), the new row is `new AgentPrompt { Id = Guid.CreateVersion7(), ... }`, inside the same transaction as the deactivation (the unique partial index rejects a second active row if the deactivation is missing or runs after the insert).
252
254
 
253
255
  ---
254
256
 
@@ -1,7 +1,7 @@
1
1
  # Model Registry — Provider-agnostic agent configuration
2
2
 
3
3
  > **Scope:** stacks=["dotnet"]
4
- > **Layer:** 0 (always-load via persona maf-expert)
4
+ > **Layer:** 0 (lista da persona `maf-expert`; resumo injetado pela rule `ai-agents-standards` em `**/Agents/**`, `**/agents/**`, `**/*Agent*.cs`; texto completo sob demanda via `morph-spec standards --find`)
5
5
  > **Keywords:** model registry, provider agnostic, model alias, IChatClient factory, multi-model, model selection, model config, api per alias, responses vs chat, reasoning effort
6
6
  > **Read by Claude in:** plan (cita este standard quando feature menciona >1 modelo ou multi-provider) + implement (copia o template ModelRegistry.cs e gera config no projeto)
7
7
 
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** rag, vector search, pgvector, embeddings, custom rag, retrieval, semantic search, knowledge base, RAG tool, delta ingestion, incremental ingestion, lookup key
6
6
  > **Read by Claude in:** implement (when an agent needs to retrieve knowledge)
7
7
 
8
- **Verified against:** Microsoft.Agents.AI 1.20.0 + Microsoft.Extensions.AI 10.9.0 (ai-pin 2026-09-08); `provado por templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs` **restrito à seção "Ingestão delta por lookup-key"** — é a única parte deste standard que o ai-kit compila. Todo o resto (pgvector, SQL, HNSW, embeddings, multi-tenant) é **verificação documental**; a versão e o status de `Microsoft.Extensions.DataIngestion` foram apurados no nuget.org em 2026-09-08. Last-verified: 2026-09-08.
8
+ **Verified against:** Microsoft.Agents.AI 1.20.0 + Microsoft.Extensions.AI 10.9.0 (ai-pin 2026-09-08); `provado por templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs` **restrito à seção "Ingestão delta por lookup-key"** — é a única parte deste standard que o ai-kit compila. Todo o resto (pgvector, SQL, HNSW, embeddings, multi-tenant) é **verificação documental**; a versão e o status de `Microsoft.Extensions.DataIngestion` foram apurados no nuget.org em 2026-09-08. Em 2026-10-03, contra o mesmo pin, compilaram com `TreatWarningsAsErrors` os blocos de registro do `IEmbeddingGenerator`, do `VectorSearchTool` (EF Core + Pgvector.EntityFrameworkCore 0.3.0) e do *Exemplo de uso* (a ponte `Microsoft.Extensions.AI.OpenAI` 10.9.0 resolveu `OpenAI` 2.12.0); a forma anterior `RunAsync(question, ct)` não compilava (CS1503). Last-verified: 2026-09-08.
9
9
 
10
10
  ---
11
11
 
@@ -27,7 +27,7 @@ Hosted RAG (uploading PDFs to a service) hands control of chunking, embedding, a
27
27
  [Document ingestion]
28
28
  │
29
29
  ▼
30
- EmbeddingService (generates float vectors via IEmbeddingGenerator)
30
+ IEmbeddingGenerator (injected) (batch GenerateAsync at ingestion, one vector per query)
31
31
  │
32
32
  ▼
33
33
  kb_embeddings table (pgvector column, HNSW index, tenant_id partition)
@@ -41,9 +41,9 @@ Hosted RAG (uploading PDFs to a service) hands control of chunking, embedding, a
41
41
 
42
42
  Three pieces:
43
43
 
44
- 1. **EmbeddingService** — wraps `IEmbeddingGenerator<string, Embedding<float>>` from `Microsoft.Extensions.AI`. Called during indexing (batch) and at query time (single text).
44
+ 1. **`IEmbeddingGenerator<string, Embedding<float>>`** (from `Microsoft.Extensions.AI`), injected directly where embeddings are needed: the ingestion handler (batch) and the tool (single text). No wrapper class.
45
45
  2. **`kb_embeddings` table** — stores chunk text + `vector(1536)` + `tenant_id` + `metadata jsonb`. HNSW index with `vector_cosine_ops`.
46
- 3. **VectorSearchTool** — a typed MAF tool. A `[Description]`-decorated method the agent can call. Embeds the query, runs cosine-distance SQL, returns top-k chunks as concatenated text.
46
+ 3. **VectorSearchTool** — a typed MAF tool. A `[Description]`-decorated method the agent can call. Embeds the query, orders by cosine distance (EF Core LINQ), returns top-k chunks as concatenated text.
47
47
 
48
48
  ---
49
49
 
@@ -63,37 +63,35 @@ For implementation of each strategy, see `data-vector-search-rag-chunking`. Do n
63
63
 
64
64
  ---
65
65
 
66
- ## EmbeddingService
66
+ ## Embeddings: `IEmbeddingGenerator` direto no slice
67
67
 
68
- Reference template: `templates/code/dotnet/ai-agents/EmbeddingService.cs.template`
68
+ There is **no `EmbeddingService` wrapper**. The ingestion handler and the `VectorSearchTool` inject `IEmbeddingGenerator<string, Embedding<float>>` (from `Microsoft.Extensions.AI`) directly: a class whose `EmbedAsync`/`EmbedBatchAsync` only rename `GenerateVectorAsync`/`GenerateAsync` is the service layer the stack forbids, and it hides the batch call behind a per-text API.
69
69
 
70
- The service wraps `IEmbeddingGenerator<string, Embedding<float>>` (from `Microsoft.Extensions.AI`) and exposes two methods:
70
+ - **Query time (one text):** `await embeddings.GenerateVectorAsync(query, cancellationToken: ct)` returns a `ReadOnlyMemory<float>`; wrap it in `Pgvector.Vector` for the column.
71
+ - **Indexing (batch):** `await embeddings.GenerateAsync(texts, cancellationToken: ct)` returns `GeneratedEmbeddings<Embedding<float>>`, one provider call for the whole batch; each element's `.Vector` is a `ReadOnlyMemory<float>`.
71
72
 
72
- ```csharp
73
- // Single text → embedding vector (used at query time)
74
- Task<ReadOnlyMemory<float>> EmbedAsync(string text, CancellationToken ct = default);
75
-
76
- // Batch of texts → list of vectors (used during document indexing)
77
- Task<IReadOnlyList<ReadOnlyMemory<float>>> EmbedBatchAsync(
78
- IReadOnlyList<string> texts, CancellationToken ct = default);
79
- ```
80
-
81
- Both call `GenerateAsync(IEnumerable<string>, ...)` on the injected `IEmbeddingGenerator` (verified via context7 / Microsoft Learn). `GenerateAsync` returns `GeneratedEmbeddings<Embedding<float>>`; each element's `.Vector` property is a `ReadOnlyMemory<float>`.
82
-
83
- **Wiring the `IEmbeddingGenerator`:** the project registers its chosen embedding provider in DI (e.g., via the OpenAI embedding client's `.AsIEmbeddingGenerator()` extension, or the equivalent for another provider). The `embeddings` alias in `model-registry.json` declares the model (`text-embedding-3-small`, dimensions 1536). The `EmbeddingService` is registered as a scoped or singleton service and injected wherever indexing or search happens.
73
+ **Wiring the `IEmbeddingGenerator`:** the project registers its chosen embedding provider in DI (e.g., via the OpenAI embedding client's `.AsIEmbeddingGenerator()` extension, from `Microsoft.Extensions.AI.OpenAI` -- mind the NU1608 restore trap against `OpenAI` 2.13.0 described in `ai-agents/setup.md`). The `embeddings` alias in `model-registry.json` declares the model (`text-embedding-3-small`, dimensions 1536).
84
74
 
85
75
  ```csharp
86
76
  // Example DI registration (Program.cs, in the project — not in framework)
87
- builder.Services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(sp =>
77
+ using Microsoft.Extensions.AI;
78
+ using OpenAI;
79
+
80
+ public static class EmbeddingRegistration
88
81
  {
89
- var cfg = sp.GetRequiredService<IConfiguration>();
90
- return new OpenAIClient(cfg["OpenAI:ApiKey"]!)
91
- .GetEmbeddingClient("text-embedding-3-small")
92
- .AsIEmbeddingGenerator();
93
- });
94
- builder.Services.AddScoped<EmbeddingService>();
82
+ public static IServiceCollection AddEmbeddings(this IServiceCollection services) =>
83
+ services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(sp =>
84
+ {
85
+ var cfg = sp.GetRequiredService<IConfiguration>();
86
+ return new OpenAIClient(cfg["OpenAI:ApiKey"]!)
87
+ .GetEmbeddingClient("text-embedding-3-small")
88
+ .AsIEmbeddingGenerator();
89
+ });
90
+ }
95
91
  ```
96
92
 
93
+ The legacy `templates/code/dotnet/ai-agents/EmbeddingService.cs.template` still generates the wrapper; do not copy it into new code.
94
+
97
95
  ---
98
96
 
99
97
  ## A tabela pgvector
@@ -102,7 +100,7 @@ Reference template: `templates/code/sql/ai-agents/pgvector-embeddings-migration.
102
100
 
103
101
  ```sql
104
102
  CREATE TABLE kb_embeddings (
105
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
103
+ id uuid PRIMARY KEY, -- set by the app: deterministic V5 for delta-ingested chunks (rule 3 below), otherwise Guid.CreateVersion7(); never a gen_random_uuid() default
106
104
  tenant_id uuid NOT NULL,
107
105
  source text NOT NULL, -- document identifier / URL
108
106
  chunk_index int NOT NULL, -- position within source document
@@ -135,23 +133,53 @@ Reference template: `templates/code/dotnet/ai-agents/VectorSearchTool.cs.templat
135
133
  A sealed class with a `[Description]`-decorated method the agent calls when it needs context from the knowledge base.
136
134
 
137
135
  ```csharp
138
- // Registration — in the agent setup code
139
- var tool = sp.GetRequiredService<VectorSearchTool>();
140
- var agent = _modelRegistry.GetChatClient("text-default")
141
- .AsAIAgent(
142
- instructions: systemPrompt,
143
- tools: [AIFunctionFactory.Create(tool.SearchAsync)]);
136
+ // Features/KnowledgeFeature/VectorSearchTool.cs
137
+ using System.ComponentModel;
138
+ using Microsoft.EntityFrameworkCore;
139
+ using Microsoft.Extensions.AI;
140
+ using Pgvector;
141
+ using Pgvector.EntityFrameworkCore;
142
+
143
+ public interface ITenantContext
144
+ {
145
+ Guid TenantId { get; }
146
+ }
147
+
148
+ public sealed class VectorSearchTool(
149
+ ApplicationDbContext db,
150
+ IEmbeddingGenerator<string, Embedding<float>> embeddings,
151
+ ITenantContext tenant)
152
+ {
153
+ [Description("Searches the knowledge base and returns the most relevant passages for the query.")]
154
+ public async Task<string> SearchAsync(
155
+ [Description("Natural-language search query")] string query,
156
+ CancellationToken ct = default)
157
+ {
158
+ var queryVector = new Vector(await embeddings.GenerateVectorAsync(query, cancellationToken: ct));
159
+
160
+ var chunks = await db.KbEmbeddings
161
+ .Where(e => e.TenantId == tenant.TenantId)
162
+ .OrderBy(e => e.Embedding.CosineDistance(queryVector))
163
+ .Take(5)
164
+ .Select(e => e.Content)
165
+ .ToListAsync(ct);
166
+
167
+ return string.Join("\n\n---\n\n", chunks);
168
+ }
169
+ }
144
170
  ```
145
171
 
146
172
  The tool:
147
173
  1. Takes a natural-language `query` string (the only parameter the LLM sees).
148
- 2. Calls `EmbeddingService.EmbedAsync(query)` to get a query vector.
149
- 3. Runs a parameterized SQL `SELECT content FROM kb_embeddings WHERE tenant_id = @tenant ORDER BY embedding <=> @queryVec LIMIT 5`.
174
+ 2. Calls `GenerateVectorAsync(query)` on the injected generator to get the query vector.
175
+ 3. Filters by the tenant, orders by `CosineDistance` (translated to `ORDER BY embedding <=> @query`, which uses the HNSW index) and takes 5.
150
176
  4. Concatenates chunk contents and returns them as a single string — the agent uses this as grounding context.
151
177
 
178
+ Registered on the agent with `tools: [AIFunctionFactory.Create(tool.SearchAsync)]` (see *Exemplo de uso*); `KbEmbedding` is mapped like the `Document` of `infrastructure-neon-pgvector` (`Vector` property, `HasColumnName` for the snake_case columns, `UseVector()`).
179
+
152
180
  The `tenant_id` filter is **never a tool parameter** (that would allow the LLM to cross tenant boundaries). It is injected via a constructor dependency — e.g., a scoped `ITenantContext` or a `Guid currentTenantId` resolved from the authenticated request. See §Multi-tenant below.
153
181
 
154
- Note on raw Npgsql vs EF Core: this template uses `NpgsqlDataSource` (raw SQL) for the cosine-distance query, since pgvector operators (`<=>`) are easier to express and verify in raw SQL. If the project already wires pgvector through EF Core (`UseVector()`, `HasMethod("hnsw")`), the equivalent `db.Database.SqlQuery<>` form per `infrastructure-neon-pgvector` is equally valid — choose whichever is already in use.
182
+ Note on raw Npgsql vs EF Core: the EF Core LINQ form above is the default (`ApplicationDbContext` in the slice, no raw SQL). A raw `NpgsqlDataSource` query is acceptable only with a `decisions.md` justification (e.g. a project with no EF Core model for this table). The legacy `VectorSearchTool.cs.template` still uses raw SQL and `EmbeddingService`; prefer the block above.
155
183
 
156
184
  ---
157
185
 
@@ -176,7 +204,7 @@ public sealed class KnowledgeAgentHandler(
176
204
  instructions: systemPrompt,
177
205
  tools: [AIFunctionFactory.Create(searchTool.SearchAsync)]);
178
206
 
179
- var response = await agent.RunAsync(question, ct);
207
+ var response = await agent.RunAsync(question, cancellationToken: ct);
180
208
  return response.Text;
181
209
  }
182
210
  }
@@ -283,9 +311,9 @@ ALTER TABLE kb_embeddings ENABLE ROW LEVEL SECURITY;
283
311
 
284
312
  ## Checklist (verifiable in review)
285
313
 
286
- - [ ] EmbeddingService (concrete class) registered in DI; it injects IEmbeddingGenerator<string, Embedding<float>> for the project's chosen embedding model.
314
+ - [ ] `IEmbeddingGenerator<string, Embedding<float>>` registered in DI for the project's chosen embedding model and injected directly into the ingestion handler and `VectorSearchTool` — no `EmbeddingService` wrapper.
287
315
  - [ ] Embedding model declared in `model-registry.json` under the `embeddings` alias.
288
- - [ ] `kb_embeddings` table has `embedding vector(1536)` and HNSW index with `vector_cosine_ops`.
316
+ - [ ] `kb_embeddings` table has `embedding vector(1536)` and HNSW index with `vector_cosine_ops`; `id` has no column default (app-assigned).
289
317
  - [ ] `tenant_id` column present and filtered in every query.
290
318
  - [ ] `VectorSearchTool.SearchAsync` takes only `query` + `ct` — no tenant param visible to the LLM.
291
319
  - [ ] `VectorSearchTool` registered on the agent via `AIFunctionFactory.Create(tool.SearchAsync)`.
@@ -1,7 +1,7 @@
1
1
  # Microsoft Agent Framework — Setup Guide (.NET 10)
2
2
 
3
3
  > **Scope:** stacks=["dotnet"]
4
- > **Layer:** 0 (always-load via persona maf-expert)
4
+ > **Layer:** 0 (lista da persona `maf-expert`; resumo injetado pela rule `ai-agents-standards` em `**/Agents/**`, `**/agents/**`, `**/*Agent*.cs`; texto completo sob demanda via `morph-spec standards --find`)
5
5
  > **Keywords:** maf setup, microsoft agents ai, chatclient, ai agent, agent framework, agent dotnet, AsAIAgent, responses api, chat completions, store false
6
6
  > **Read by Claude in:** plan (pra citar pacote em tasks.json) + implement (pra escrever .csproj e Program.cs)
7
7
 
@@ -1,7 +1,7 @@
1
1
  # Structured Output — Typed agent responses
2
2
 
3
3
  > **Scope:** stacks=["dotnet"]
4
- > **Layer:** 0 (always-load via persona maf-expert)
4
+ > **Layer:** 0 (lista da persona `maf-expert`; resumo injetado pela rule `ai-agents-standards` em `**/Agents/**`, `**/agents/**`, `**/*Agent*.cs`; texto completo sob demanda via `morph-spec standards --find`)
5
5
  > **Keywords:** structured output, typed response, response format, schema validated output, record output, JSON schema agent, RunAsync generic
6
6
  > **Read by Claude in:** implement (sempre que um agente produz saída consumida por outro sistema)
7
7
 
@@ -1,7 +1,7 @@
1
1
  # Sweet Spot — When to use what in MAF
2
2
 
3
3
  > **Scope:** stacks=["dotnet"]
4
- > **Layer:** 0 (always-load via persona maf-expert)
4
+ > **Layer:** 0 (lista da persona `maf-expert`; resumo injetado pela rule `ai-agents-standards` em `**/Agents/**`, `**/agents/**`, `**/*Agent*.cs`; texto completo sob demanda via `morph-spec standards --find`)
5
5
  > **Keywords:** sweet spot, when to use agent, agent vs workflow, when not to use workflow, MAF decision, simple agent, tool budget, quantas tools
6
6
  > **Read by Claude in:** plan (decide arquitetura da feature antes de quebrar em tasks) + implement (validar que a arquitetura ainda é a mais simples possível)
7
7
 
@@ -1,7 +1,7 @@
1
1
  # Comentários no código
2
2
 
3
3
  > **Scope:** universal
4
- > **Layer:** 0 (always load)
4
+ > **Layer:** 0 (resumo injetado pelas rules `csharp-standards` (`**/*.cs`, `**/*.csproj`) e `nextjs-standards` (`**/*.ts`, `**/*.tsx`); texto completo sob demanda via `morph-spec standards --find`)
5
5
  > **Keywords:** comentário, comentario, comment, docstring, jsdoc, xml doc, história da task, código autoexplicativo
6
6
  > **Load When:** escrevendo ou revisando código em qualquer stack
7
7
 
@@ -97,7 +97,7 @@ processo, banner/`#region`, bloco de mais de 3 linhas ou `///`/JSDoc em projeto
97
97
  publicado. Comentário que o arquivo já tinha nunca é acusado. Repetir o código em uma linha não é
98
98
  detectável por forma: fica com a revisão abaixo.
99
99
 
100
- Comentário que o código dispensava é achado da rubrica `code-quality` no Gate 3. O nó `validators`
100
+ Comentário que o código dispensava é nota do avaliador no Gate 3 (`review-checklist.md`), não bloqueador. O nó `validators`
101
101
  do `morph-spec verify` mede, nas linhas que a feature adicionou (validador `code-budget`), quantas
102
102
  linhas de comentário citam processo e a densidade de comentário sobre o código adicionado, e avisa
103
103
  com os números e os arquivos mais comentados.
@@ -4,7 +4,7 @@
4
4
  > **Layer:** 2
5
5
  > **Keywords:** vertical-slice, vsa, minimal-api, ihandler, result-pattern, feature-slice, scrutor, fluentvalidation
6
6
 
7
- **Verified against:** .NET 10 + EF Core 10 + FluentValidation 12 + Scrutor 7. Last-verified: 2026-05-20.
7
+ **Verified against:** .NET 10 (SDK 10.0.400-preview) + EF Core 10.0.12 + FluentValidation 12.1.1 + Scrutor 7. Os blocos da slice (Handler, Validator, Endpoint) compilam com `TreatWarningsAsErrors` sobre o `Result`/`Error`/`ErrorHttpMapping` de `result-pattern.md` nos namespaces da árvore acima (`{ProjectName}` substituído; o Handler com `global using` de Abstractions/Database/Entities, o Endpoint só com os próprios `using`); Abstractions, Pipelines e DI são verificação documental. Last-verified: 2026-10-03.
8
8
 
9
9
  ---
10
10
 
@@ -37,7 +37,8 @@ Sem MediatR, sem AggregateRoot, sem Domain Events, sem CQRS formal, sem reposito
37
37
  ├── Extensions/
38
38
  │ ├── HandlerRegistrationExtensions.cs # AddHandlersFromAssembly() com Scrutor Decorate
39
39
  │ ├── MapEndpointExtensions.cs # RegisterApiEndpointsFromAssembly() + MapApiEndpoints()
40
- │ └── ResultExtensions.cs # result.Match(onSuccess, onFailure)
40
+ │ ├── ResultExtensions.cs # result.Match(onSuccess, onFailure)
41
+ │ └── ErrorHttpMapping.cs # error.ToHttpResult(): ErrorType → status + ProblemDetails
41
42
  ├── Features/
42
43
  │ └── {Entity}Feature/
43
44
  │ ├── {Entity}Errors.cs # static class {Entity}Errors (factory methods)
@@ -215,9 +216,9 @@ public sealed class CreateBookHandler(
215
216
  using FluentValidation;
216
217
  namespace {ProjectName}.Features.BookFeature.CreateBook;
217
218
 
218
- public class CreateBookValidator : AbstractValidator<CreateBookRequest>
219
+ public sealed class CreateBookValidator : AbstractValidator<CreateBookRequest>
219
220
  {
220
- public CreateBookValidator()
221
+ public CreateBookValidator(TimeProvider timeProvider)
221
222
  {
222
223
  RuleFor(c => c.Title).NotEmpty().MaximumLength(200);
223
224
  RuleFor(c => c.Author).NotEmpty().MaximumLength(100);
@@ -225,7 +226,7 @@ public class CreateBookValidator : AbstractValidator<CreateBookRequest>
225
226
  RuleFor(c => c.Price).GreaterThan(0);
226
227
  RuleFor(c => c.PublishedYear)
227
228
  .GreaterThan(1000)
228
- .LessThanOrEqualTo(DateTime.UtcNow.Year);
229
+ .LessThanOrEqualTo(_ => timeProvider.GetUtcNow().Year);
229
230
  }
230
231
  }
231
232
  ```
@@ -234,6 +235,9 @@ public class CreateBookValidator : AbstractValidator<CreateBookRequest>
234
235
 
235
236
  ```csharp
236
237
  // Features/BookFeature/CreateBook/CreateBookEndpoint.cs
238
+ using {ProjectName}.Abstractions;
239
+ using {ProjectName}.Constants;
240
+ using {ProjectName}.Extensions;
237
241
  namespace {ProjectName}.Features.BookFeature.CreateBook;
238
242
 
239
243
  internal sealed class CreateBookEndpoint : IApiEndpoint
@@ -247,16 +251,18 @@ internal sealed class CreateBookEndpoint : IApiEndpoint
247
251
  {
248
252
  var result = await handler.HandleAsync(command, cancellationToken);
249
253
  return result.Match(
250
- onSuccess: () => Results.Ok(result.Value),
251
- onFailure: error => Results.BadRequest(error));
254
+ onSuccess: response => Results.Ok(response),
255
+ onFailure: error => error.ToHttpResult());
252
256
  })
253
257
  .WithTags(ApiTags.Books)
254
258
  .Produces<CreateBookResponse>(StatusCodes.Status200OK)
255
- .Produces(StatusCodes.Status400BadRequest);
259
+ .ProducesProblem(StatusCodes.Status400BadRequest);
256
260
  }
257
261
  }
258
262
  ```
259
263
 
264
+ O lado da falha passa por `error.ToHttpResult()` (`Extensions/ErrorHttpMapping.cs`, código em `backend/dotnet/result-pattern.md` → *Match() Pattern*): o `ErrorType` decide o status (`NotFound` → 404, `Conflict` → 409, `Validation` → 400 com `errors[]`) e a resposta é ProblemDetails. `Results.BadRequest(error)` para todo erro transforma entidade ausente e conflito em 400. O sucesso recebe o valor no lambda: o endpoint nunca lê `result.Value`. Declare um `.ProducesProblem(status)` para cada status de falha que a slice pode devolver.
265
+
260
266
  ### 4. Errors (por feature, não por operação)
261
267
 
262
268
  ```csharp
@@ -342,6 +348,9 @@ builder.Services.AddSQLDatabaseConfiguration(builder.Configuration);
342
348
  // Endpoints (auto-discovery via reflection)
343
349
  builder.Services.RegisterApiEndpointsFromAssembly(Assembly.GetExecutingAssembly());
344
350
 
351
+ // Relógio injetável (validators e handlers leem TimeProvider, nunca DateTime.UtcNow)
352
+ builder.Services.AddSingleton(TimeProvider.System);
353
+
345
354
  // FluentValidation (auto-discovery)
346
355
  builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
347
356
 
@@ -380,7 +389,7 @@ app.MapApiEndpoints();
380
389
  1. **Handler records no mesmo arquivo**: `Request` e `Response` são declarados no mesmo arquivo `.cs` que o `Handler`.
381
390
  2. **`sealed` em tudo**: handlers, endpoints, validators, request/response records usam `sealed`.
382
391
  3. **`Guid.CreateVersion7()`** para gerar IDs (não `Guid.NewGuid()`).
383
- 4. **`result.Match()`** nos endpoints — nunca acessar `result.Value` sem checar `IsSuccess`.
392
+ 4. **`result.Match()`** nos endpoints — o sucesso recebe o valor no lambda (nunca ler `result.Value`) e a falha vira `error.ToHttpResult()` (status pelo `ErrorType`, nunca `Results.BadRequest` para todo erro).
384
393
  5. **IApiEndpoint `internal sealed`** — endpoints não são públicos.
385
394
  6. **`{Entity}Errors` compartilhado** por todas as operações da feature (1 arquivo por feature, não por slice).
386
395
  7. **`ApplicationDbContext` direto no slice**: CRUD por chave via `DbSet<T>`; queries específicas (joins, projeção, paginação) via LINQ no handler. Nunca uma Application Service layer entre handler e dados; nunca repository genérico (`IRepository<T>`/`IUnitOfWork`).