@polymorphism-tech/morph-spec 7.7.3 → 7.8.1

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 (187) hide show
  1. package/README.md +2 -2
  2. package/bin/validate.js +379 -379
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +1 -1
  7. package/framework/MORPH.md +3 -2
  8. package/framework/agents.json +1 -1
  9. package/framework/commands/commit.md +166 -166
  10. package/framework/commands/morph-infra.md +215 -215
  11. package/framework/commands/morph-preflight.md +205 -205
  12. package/framework/commands/morph-troubleshoot.md +124 -124
  13. package/framework/evals/scenarios/dotnet-crud.yaml +1 -1
  14. package/framework/hooks/claude-code/statusline.py +679 -679
  15. package/framework/hooks/claude-code/statusline.sh +11 -11
  16. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  17. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  18. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  19. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  20. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  21. package/framework/hooks/shared/activity-logger.js +160 -160
  22. package/framework/hooks/shared/hook-response.js +45 -45
  23. package/framework/hooks/shared/payload-utils.js +78 -78
  24. package/framework/hooks/shared/stdin-reader.js +26 -26
  25. package/framework/rubrics/backend.json +41 -41
  26. package/framework/rules/ai-agents-standards.md +45 -45
  27. package/framework/rules/dotnet-templates.md +86 -87
  28. package/framework/rules/frontend-standards.md +13 -13
  29. package/framework/rules/infrastructure-standards.md +27 -27
  30. package/framework/rules/neon-standards.md +25 -25
  31. package/framework/rules/nextjs-standards.md +20 -20
  32. package/framework/rules/nextjs-templates.md +40 -40
  33. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  34. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  35. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  36. package/framework/skills/core/morph-brainstorming/SKILL.md +9 -5
  37. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  38. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  39. package/framework/skills/core/morph-plan/SKILL.md +11 -6
  40. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  41. package/framework/skills/core/morph-review/SKILL.md +13 -7
  42. package/framework/skills/core/morph-stack-scan/SKILL.md +5 -5
  43. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  44. package/framework/standards/STANDARDS.json +1 -1
  45. package/framework/standards/ai-agents/agent-session.md +160 -160
  46. package/framework/standards/ai-agents/batching.md +193 -193
  47. package/framework/standards/ai-agents/context-providers.md +188 -188
  48. package/framework/standards/ai-agents/mcp-server.md +250 -250
  49. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  50. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  51. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  52. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  53. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  54. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  55. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  56. package/framework/standards/ai-agents/production.md +275 -275
  57. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  58. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  59. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  60. package/framework/standards/ai-agents/structured-output.md +249 -249
  61. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  62. package/framework/standards/architecture/vertical-slice/vertical-slice.md +34 -35
  63. package/framework/standards/backend/api/validation.md +2 -2
  64. package/framework/standards/backend/dotnet/scrutor.md +1 -3
  65. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +42 -37
  66. package/framework/standards/data/nosql/cache/redis.md +2 -2
  67. package/framework/templates/CLAUDE.md.core.template +1 -1
  68. package/framework/templates/IDE-SNIPPETS.md +262 -262
  69. package/framework/templates/README.md +0 -1
  70. package/framework/templates/REGISTRY.json +0 -21
  71. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  72. package/framework/templates/code/dotnet/abstractions/handler.cs +0 -3
  73. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  74. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  75. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  76. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  77. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  78. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  79. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  80. package/framework/templates/code/dotnet/constants/api-tags.cs +1 -1
  81. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -282
  82. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  83. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  84. package/framework/templates/code/dotnet/feature/handler.cs.hbs +23 -24
  85. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  86. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  87. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  88. package/framework/templates/code/dotnet/jobs/job.cs +15 -15
  89. package/framework/templates/code/dotnet/test.cs +72 -101
  90. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  91. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  92. package/framework/templates/code/typescript/contracts.ts +167 -167
  93. package/framework/templates/docs/clarifications.md +253 -253
  94. package/framework/templates/docs/schema-analysis.md +119 -119
  95. package/framework/templates/docs/ui-components.md +52 -52
  96. package/framework/templates/docs/ui-design-system.md +76 -76
  97. package/framework/templates/docs/ui-flows.md +167 -167
  98. package/framework/templates/docs/ui-mockups.md +98 -98
  99. package/framework/templates/feature/decisions.md +122 -122
  100. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  101. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  102. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  103. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  104. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  105. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  106. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  107. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  108. package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
  109. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  110. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  111. package/framework/templates/infrastructure/github/README.md +56 -56
  112. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  113. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  114. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  115. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  116. package/framework/templates/integrations/asaas-client.cs +387 -387
  117. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  118. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  119. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  120. package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -113
  121. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  122. package/framework/templates/project/validate.js +120 -120
  123. package/framework/templates/state.template.json +17 -17
  124. package/framework/templates/ui/design-system.css +226 -226
  125. package/package.json +1 -1
  126. package/src/commands/dashboard/dashboard.js +53 -53
  127. package/src/commands/index.js +13 -13
  128. package/src/commands/mcp/mcp-setup.js +519 -519
  129. package/src/commands/project/checkpoint.js +209 -209
  130. package/src/commands/project/update.js +7 -4
  131. package/src/core/state/phase-state-machine.js +197 -197
  132. package/src/core/templates/template-renderer.js +477 -477
  133. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  134. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  135. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  136. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  137. package/src/dashboard/app/main.jsx +13 -13
  138. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  139. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  140. package/src/dashboard/app/pages/LiveState.jsx +239 -239
  141. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  142. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  143. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  144. package/src/dashboard/app/pages/Standards.jsx +332 -332
  145. package/src/dashboard/app/styles/index.css +47 -47
  146. package/src/dashboard/index.html +12 -12
  147. package/src/dashboard/package.json +24 -24
  148. package/src/dashboard/scanner/hooks-scanner.js +41 -41
  149. package/src/dashboard/scanner/phases-scanner.js +15 -15
  150. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  151. package/src/dashboard/scanner/rules-scanner.js +37 -37
  152. package/src/dashboard/scanner/skills-scanner.js +43 -43
  153. package/src/dashboard/scanner/squads-scanner.js +13 -13
  154. package/src/dashboard/scanner/standards-scanner.js +26 -26
  155. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  156. package/src/dashboard/vite.config.js +24 -24
  157. package/src/lib/agents/metrics-aggregator.js +56 -56
  158. package/src/lib/agents/metrics-calculator.js +125 -125
  159. package/src/lib/agents/output-collector.js +73 -73
  160. package/src/lib/detectors/claude-config-detector.js +152 -152
  161. package/src/lib/detectors/design-system-detector.js +231 -231
  162. package/src/lib/detectors/index.js +217 -217
  163. package/src/lib/generators/architecture-scanner.js +639 -639
  164. package/src/lib/generators/design-system-generator.js +508 -508
  165. package/src/lib/generators/index.js +13 -13
  166. package/src/lib/generators/settings-generator.js +209 -209
  167. package/src/lib/hooks/hook-executor.js +167 -167
  168. package/src/lib/installers/mcp-installer.js +719 -719
  169. package/src/lib/tasks/task-classifier.js +30 -30
  170. package/src/lib/tasks/task-parser.js +209 -209
  171. package/src/lib/tasks/test-runner.js +81 -81
  172. package/src/lib/validators/architecture/architecture-validator.js +259 -259
  173. package/src/lib/validators/checkpoint-persistence.js +39 -39
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
  175. package/src/lib/validators/css/css-validator.js +340 -340
  176. package/src/lib/validators/packages/package-validator.js +347 -347
  177. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  178. package/src/lib/validators/shared/issue-counter.js +14 -14
  179. package/src/lib/validators/validation-runner.js +349 -349
  180. package/src/scripts/setup-infra.js +383 -383
  181. package/src/utils/agent-id-mapper.js +11 -11
  182. package/src/utils/banner.js +52 -43
  183. package/src/utils/claude-settings-manager.js +300 -300
  184. package/src/utils/skills-installer.js +172 -172
  185. package/framework/templates/.idea/morph-templates.xml +0 -92
  186. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
  187. package/framework/templates/code/dotnet/backend/repository.cs +0 -141
@@ -6,9 +6,10 @@
6
6
  using FluentAssertions;
7
7
  using FluentValidation;
8
8
  using FluentValidation.Results;
9
- using Microsoft.Extensions.Logging;
10
- using NSubstitute;
11
- using {{pascalCase PROJECT_NAME}}.Repository;
9
+ using Microsoft.Data.Sqlite;
10
+ using Microsoft.EntityFrameworkCore;
11
+ using {{pascalCase PROJECT_NAME}}.Database;
12
+ using {{pascalCase PROJECT_NAME}}.Entities;
12
13
  using Xunit;
13
14
 
14
15
  namespace {{pascalCase PROJECT_NAME}}.Tests.Unit.Features.{{pascalCase FEATURE_NAME}};
@@ -19,19 +20,31 @@ namespace {{pascalCase PROJECT_NAME}}.Tests.Unit.Features.{{pascalCase FEATURE_N
19
20
 
20
21
  /// <summary>
21
22
  /// Unit tests for {{pascalCase FEATURE_NAME}} handlers.
22
- /// Tests handlers directly with mocked IRepository and IUnitOfWork.
23
+ /// Tests handlers directly against a real ApplicationDbContext backed by
24
+ /// in-memory SQLite (no mocks — DbSet is the repository, DbContext is the unit of work).
23
25
  /// </summary>
24
- public class {{pascalCase FEATURE_NAME}}HandlerTests
26
+ public class {{pascalCase FEATURE_NAME}}HandlerTests : IDisposable
25
27
  {
26
- private readonly IRepository<{{pascalCase FEATURE_NAME}}> _repository;
27
- private readonly IUnitOfWork _unitOfWork;
28
- private readonly ILogger<Create{{pascalCase FEATURE_NAME}}.Handler> _logger;
28
+ private readonly SqliteConnection _connection;
29
+ private readonly ApplicationDbContext _db;
29
30
 
30
31
  public {{pascalCase FEATURE_NAME}}HandlerTests()
31
32
  {
32
- _repository = Substitute.For<IRepository<{{pascalCase FEATURE_NAME}}>>();
33
- _unitOfWork = Substitute.For<IUnitOfWork>();
34
- _logger = Substitute.For<ILogger<Create{{pascalCase FEATURE_NAME}}.Handler>>();
33
+ _connection = new SqliteConnection("DataSource=:memory:");
34
+ _connection.Open();
35
+
36
+ var options = new DbContextOptionsBuilder<ApplicationDbContext>()
37
+ .UseSqlite(_connection)
38
+ .Options;
39
+
40
+ _db = new ApplicationDbContext(options);
41
+ _db.Database.EnsureCreated();
42
+ }
43
+
44
+ public void Dispose()
45
+ {
46
+ _db.Dispose();
47
+ _connection.Dispose();
35
48
  }
36
49
 
37
50
  #region Create Handler Tests
@@ -40,14 +53,8 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
40
53
  public async Task Create_HandleAsync_WithValidCommand_ReturnsSuccessResult()
41
54
  {
42
55
  // Arrange
43
- var command = new Create{{pascalCase FEATURE_NAME}}.Command("Test {{pascalCase FEATURE_NAME}}");
44
- var handler = new Create{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork, _logger);
45
-
46
- _repository.AddAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>())
47
- .Returns(callInfo => callInfo.Arg<{{pascalCase FEATURE_NAME}}>());
48
-
49
- _unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
50
- .Returns(1);
56
+ var command = new Create{{pascalCase FEATURE_NAME}}Request("Test {{pascalCase FEATURE_NAME}}");
57
+ var handler = new Create{{pascalCase FEATURE_NAME}}Handler(_db);
51
58
 
52
59
  // Act
53
60
  var result = await handler.HandleAsync(command, CancellationToken.None);
@@ -55,31 +62,9 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
55
62
  // Assert
56
63
  result.IsSuccess.Should().BeTrue();
57
64
  result.Value.Should().NotBeNull();
58
- result.Value.Name.Should().Be("Test {{pascalCase FEATURE_NAME}}");
59
65
 
60
- await _repository.Received(1).AddAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>());
61
- await _unitOfWork.Received(1).CommitAsync(Arg.Any<CancellationToken>());
62
- }
63
-
64
- [Fact]
65
- public async Task Create_HandleAsync_WhenCommitFails_ReturnsFailureResult()
66
- {
67
- // Arrange
68
- var command = new Create{{pascalCase FEATURE_NAME}}.Command("Test {{pascalCase FEATURE_NAME}}");
69
- var handler = new Create{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork, _logger);
70
-
71
- _repository.AddAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>())
72
- .Returns(callInfo => callInfo.Arg<{{pascalCase FEATURE_NAME}}>());
73
-
74
- _unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
75
- .ThrowsAsync(new InvalidOperationException("Database error"));
76
-
77
- // Act
78
- var act = async () => await handler.HandleAsync(command, CancellationToken.None);
79
-
80
- // Assert
81
- await act.Should().ThrowAsync<InvalidOperationException>()
82
- .WithMessage("Database error");
66
+ var persisted = await _db.Set<{{pascalCase FEATURE_NAME}}>().SingleAsync();
67
+ persisted.Name.Should().Be("Test {{pascalCase FEATURE_NAME}}");
83
68
  }
84
69
 
85
70
  #endregion
@@ -90,12 +75,12 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
90
75
  public async Task GetById_HandleAsync_WithValidId_ReturnsSuccessResult()
91
76
  {
92
77
  // Arrange
93
- var query = new GetById{{pascalCase FEATURE_NAME}}.Query(1);
94
- var entity = CreateTestEntity(1, "Test {{pascalCase FEATURE_NAME}}");
95
- var handler = new GetById{{pascalCase FEATURE_NAME}}.Handler(_repository);
78
+ var entity = CreateTestEntity("Test {{pascalCase FEATURE_NAME}}");
79
+ _db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
80
+ await _db.SaveChangesAsync();
96
81
 
97
- _repository.GetByIdAsync(1, Arg.Any<CancellationToken>())
98
- .Returns(entity);
82
+ var query = new Get{{pascalCase FEATURE_NAME}}ByIdRequest(entity.Id);
83
+ var handler = new Get{{pascalCase FEATURE_NAME}}ByIdHandler(_db);
99
84
 
100
85
  // Act
101
86
  var result = await handler.HandleAsync(query, CancellationToken.None);
@@ -110,11 +95,8 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
110
95
  public async Task GetById_HandleAsync_WithInvalidId_ReturnsFailureResult()
111
96
  {
112
97
  // Arrange
113
- var query = new GetById{{pascalCase FEATURE_NAME}}.Query(999);
114
- var handler = new GetById{{pascalCase FEATURE_NAME}}.Handler(_repository);
115
-
116
- _repository.GetByIdAsync(999, Arg.Any<CancellationToken>())
117
- .Returns(({{pascalCase FEATURE_NAME}}?)null);
98
+ var query = new Get{{pascalCase FEATURE_NAME}}ByIdRequest(Guid.CreateVersion7());
99
+ var handler = new Get{{pascalCase FEATURE_NAME}}ByIdHandler(_db);
118
100
 
119
101
  // Act
120
102
  var result = await handler.HandleAsync(query, CancellationToken.None);
@@ -131,15 +113,12 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
131
113
  public async Task Update_HandleAsync_WithValidCommand_ReturnsSuccessResult()
132
114
  {
133
115
  // Arrange
134
- var command = new Update{{pascalCase FEATURE_NAME}}.Command(1, "Updated Name");
135
- var entity = CreateTestEntity(1, "Original Name");
136
- var handler = new Update{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork);
137
-
138
- _repository.GetByIdAsync(1, Arg.Any<CancellationToken>())
139
- .Returns(entity);
116
+ var entity = CreateTestEntity("Original Name");
117
+ _db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
118
+ await _db.SaveChangesAsync();
140
119
 
141
- _unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
142
- .Returns(1);
120
+ var command = new Update{{pascalCase FEATURE_NAME}}Request(entity.Id, "Updated Name");
121
+ var handler = new Update{{pascalCase FEATURE_NAME}}Handler(_db);
143
122
 
144
123
  // Act
145
124
  var result = await handler.HandleAsync(command, CancellationToken.None);
@@ -147,26 +126,22 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
147
126
  // Assert
148
127
  result.IsSuccess.Should().BeTrue();
149
128
 
150
- await _repository.Received(1).UpdateAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>());
151
- await _unitOfWork.Received(1).CommitAsync(Arg.Any<CancellationToken>());
129
+ var persisted = await _db.Set<{{pascalCase FEATURE_NAME}}>().SingleAsync(x => x.Id == entity.Id);
130
+ persisted.Name.Should().Be("Updated Name");
152
131
  }
153
132
 
154
133
  [Fact]
155
134
  public async Task Update_HandleAsync_WithNonExistentId_ReturnsFailureResult()
156
135
  {
157
136
  // Arrange
158
- var command = new Update{{pascalCase FEATURE_NAME}}.Command(999, "Updated");
159
- var handler = new Update{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork);
160
-
161
- _repository.GetByIdAsync(999, Arg.Any<CancellationToken>())
162
- .Returns(({{pascalCase FEATURE_NAME}}?)null);
137
+ var command = new Update{{pascalCase FEATURE_NAME}}Request(Guid.CreateVersion7(), "Updated");
138
+ var handler = new Update{{pascalCase FEATURE_NAME}}Handler(_db);
163
139
 
164
140
  // Act
165
141
  var result = await handler.HandleAsync(command, CancellationToken.None);
166
142
 
167
143
  // Assert
168
144
  result.IsSuccess.Should().BeFalse();
169
- await _unitOfWork.DidNotReceive().CommitAsync(Arg.Any<CancellationToken>());
170
145
  }
171
146
 
172
147
  #endregion
@@ -177,15 +152,12 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
177
152
  public async Task Delete_HandleAsync_WithValidId_ReturnsSuccessResult()
178
153
  {
179
154
  // Arrange
180
- var command = new Delete{{pascalCase FEATURE_NAME}}.Command(1);
181
- var entity = CreateTestEntity(1, "To Delete");
182
- var handler = new Delete{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork);
155
+ var entity = CreateTestEntity("To Delete");
156
+ _db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
157
+ await _db.SaveChangesAsync();
183
158
 
184
- _repository.GetByIdAsync(1, Arg.Any<CancellationToken>())
185
- .Returns(entity);
186
-
187
- _unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
188
- .Returns(1);
159
+ var command = new Delete{{pascalCase FEATURE_NAME}}Request(entity.Id);
160
+ var handler = new Delete{{pascalCase FEATURE_NAME}}Handler(_db);
189
161
 
190
162
  // Act
191
163
  var result = await handler.HandleAsync(command, CancellationToken.None);
@@ -193,36 +165,35 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
193
165
  // Assert
194
166
  result.IsSuccess.Should().BeTrue();
195
167
 
196
- await _repository.Received(1).DeleteAsync(entity, Arg.Any<CancellationToken>());
197
- await _unitOfWork.Received(1).CommitAsync(Arg.Any<CancellationToken>());
168
+ (await _db.Set<{{pascalCase FEATURE_NAME}}>().AnyAsync(x => x.Id == entity.Id)).Should().BeFalse();
198
169
  }
199
170
 
200
171
  [Fact]
201
172
  public async Task Delete_HandleAsync_WithNonExistentId_ReturnsFailureResult()
202
173
  {
203
174
  // Arrange
204
- var command = new Delete{{pascalCase FEATURE_NAME}}.Command(999);
205
- var handler = new Delete{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork);
206
-
207
- _repository.GetByIdAsync(999, Arg.Any<CancellationToken>())
208
- .Returns(({{pascalCase FEATURE_NAME}}?)null);
175
+ var command = new Delete{{pascalCase FEATURE_NAME}}Request(Guid.CreateVersion7());
176
+ var handler = new Delete{{pascalCase FEATURE_NAME}}Handler(_db);
209
177
 
210
178
  // Act
211
179
  var result = await handler.HandleAsync(command, CancellationToken.None);
212
180
 
213
181
  // Assert
214
182
  result.IsSuccess.Should().BeFalse();
215
- await _unitOfWork.DidNotReceive().CommitAsync(Arg.Any<CancellationToken>());
216
183
  }
217
184
 
218
185
  #endregion
219
186
 
220
187
  #region Helper Methods
221
188
 
222
- private static {{pascalCase FEATURE_NAME}} CreateTestEntity(int id, string name)
189
+ private static {{pascalCase FEATURE_NAME}} CreateTestEntity(string name)
223
190
  {
224
- return {{pascalCase FEATURE_NAME}}.Create(name);
225
- // Note: In real tests, use reflection or a test factory to set the Id
191
+ return new {{pascalCase FEATURE_NAME}}
192
+ {
193
+ Id = Guid.CreateVersion7(),
194
+ Name = name,
195
+ // TODO: preencher demais campos obrigatórios da entidade
196
+ };
226
197
  }
227
198
 
228
199
  #endregion
@@ -244,8 +215,8 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
244
215
  public async Task CreateValidator_WithValidCommand_PassesValidation()
245
216
  {
246
217
  // Arrange
247
- var validator = new Create{{pascalCase FEATURE_NAME}}.Validator();
248
- var command = new Create{{pascalCase FEATURE_NAME}}.Command("Valid Name");
218
+ var validator = new Create{{pascalCase FEATURE_NAME}}Validator();
219
+ var command = new Create{{pascalCase FEATURE_NAME}}Request("Valid Name");
249
220
 
250
221
  // Act
251
222
  ValidationResult result = await validator.ValidateAsync(command);
@@ -262,30 +233,30 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
262
233
  public async Task CreateValidator_WithInvalidName_FailsValidation(string? name)
263
234
  {
264
235
  // Arrange
265
- var validator = new Create{{pascalCase FEATURE_NAME}}.Validator();
266
- var command = new Create{{pascalCase FEATURE_NAME}}.Command(name!);
236
+ var validator = new Create{{pascalCase FEATURE_NAME}}Validator();
237
+ var command = new Create{{pascalCase FEATURE_NAME}}Request(name!);
267
238
 
268
239
  // Act
269
240
  ValidationResult result = await validator.ValidateAsync(command);
270
241
 
271
242
  // Assert
272
243
  result.IsValid.Should().BeFalse();
273
- result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}.Command.Name));
244
+ result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}Request.Name));
274
245
  }
275
246
 
276
247
  [Fact]
277
248
  public async Task CreateValidator_WithNameExceedingMaxLength_FailsValidation()
278
249
  {
279
250
  // Arrange
280
- var validator = new Create{{pascalCase FEATURE_NAME}}.Validator();
281
- var command = new Create{{pascalCase FEATURE_NAME}}.Command(new string('A', 201));
251
+ var validator = new Create{{pascalCase FEATURE_NAME}}Validator();
252
+ var command = new Create{{pascalCase FEATURE_NAME}}Request(new string('A', 201));
282
253
 
283
254
  // Act
284
255
  ValidationResult result = await validator.ValidateAsync(command);
285
256
 
286
257
  // Assert
287
258
  result.IsValid.Should().BeFalse();
288
- result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}.Command.Name));
259
+ result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}Request.Name));
289
260
  }
290
261
 
291
262
  #endregion
@@ -296,8 +267,8 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
296
267
  public async Task UpdateValidator_WithValidCommand_PassesValidation()
297
268
  {
298
269
  // Arrange
299
- var validator = new Update{{pascalCase FEATURE_NAME}}.Validator();
300
- var command = new Update{{pascalCase FEATURE_NAME}}.Command(1, "Updated Name");
270
+ var validator = new Update{{pascalCase FEATURE_NAME}}Validator();
271
+ var command = new Update{{pascalCase FEATURE_NAME}}Request(Guid.CreateVersion7(), "Updated Name");
301
272
 
302
273
  // Act
303
274
  ValidationResult result = await validator.ValidateAsync(command);
@@ -310,15 +281,15 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
310
281
  public async Task UpdateValidator_WithInvalidId_FailsValidation()
311
282
  {
312
283
  // Arrange
313
- var validator = new Update{{pascalCase FEATURE_NAME}}.Validator();
314
- var command = new Update{{pascalCase FEATURE_NAME}}.Command(0, "Updated Name");
284
+ var validator = new Update{{pascalCase FEATURE_NAME}}Validator();
285
+ var command = new Update{{pascalCase FEATURE_NAME}}Request(Guid.Empty, "Updated Name");
315
286
 
316
287
  // Act
317
288
  ValidationResult result = await validator.ValidateAsync(command);
318
289
 
319
290
  // Assert
320
291
  result.IsValid.Should().BeFalse();
321
- result.Errors.Should().Contain(e => e.PropertyName == nameof(Update{{pascalCase FEATURE_NAME}}.Command.Id));
292
+ result.Errors.Should().Contain(e => e.PropertyName == nameof(Update{{pascalCase FEATURE_NAME}}Request.Id));
322
293
  }
323
294
 
324
295
  #endregion
@@ -1,58 +1,58 @@
1
- -- TEMPLATE: agent-prompts-migration.sql
2
- -- PURPOSE: Creates the agent_prompts table for runtime-editable, versioned agent instructions.
3
- -- READS STANDARD: ai-agents-prompts-in-db
4
- -- PLACEHOLDERS:
5
- -- {{SCHEMA}} — target schema (default: public)
6
- -- LAST-VERIFIED: 2026-05-19
7
-
8
- -- ============================================================
9
- -- 1. TABLE
10
- -- ============================================================
11
-
12
- CREATE TABLE {{SCHEMA}}.agent_prompts (
13
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
14
- agent_key text NOT NULL,
15
- version int NOT NULL,
16
- content text NOT NULL,
17
- is_active boolean NOT NULL DEFAULT false,
18
- created_at timestamptz NOT NULL DEFAULT now()
19
- );
20
-
21
- COMMENT ON TABLE {{SCHEMA}}.agent_prompts IS 'Runtime-editable, versioned system prompts for AI agents.';
22
- COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.agent_key IS 'Logical identifier for the agent (e.g. ''proposal-agent'').';
23
- COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.version IS 'Monotonically increasing per agent_key. Never reuse a version number.';
24
- COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.content IS 'Immutable system-prompt text. UPDATE of this column is forbidden — insert a new row instead.';
25
- COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.is_active IS 'Exactly one row per agent_key may have is_active = true (enforced by ix_agent_prompts_active).';
26
- COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.created_at IS 'Audit timestamp. Set at insert time, never updated.';
27
-
28
- -- ============================================================
29
- -- 2. INDEXES
30
- -- ============================================================
31
-
32
- -- Unique partial index: enforces at DB level that only one active prompt
33
- -- exists per agent_key. Any attempt to INSERT a second active row for the
34
- -- same key will fail with a unique-constraint violation.
35
- CREATE UNIQUE INDEX ix_agent_prompts_active
36
- ON {{SCHEMA}}.agent_prompts (agent_key)
37
- WHERE is_active;
38
-
39
- -- Regular index for the PromptRepository lookup (agent_key + is_active filter).
40
- CREATE INDEX ix_agent_prompts_agent_key
41
- ON {{SCHEMA}}.agent_prompts (agent_key);
42
-
43
- -- Unique index: prevents duplicate version numbers per agent_key.
44
- CREATE UNIQUE INDEX ix_agent_prompts_agent_key_version
45
- ON {{SCHEMA}}.agent_prompts (agent_key, version);
46
-
47
- -- ============================================================
48
- -- 3. SEED (optional — remove if managed via backoffice)
49
- -- ============================================================
50
-
51
- -- INSERT INTO {{SCHEMA}}.agent_prompts (agent_key, version, content, is_active)
52
- -- VALUES ('my-agent', 1, 'You are a helpful assistant.', true);
53
-
54
- -- ============================================================
55
- -- ROLLBACK
56
- -- ============================================================
57
-
58
- -- DROP TABLE IF EXISTS {{SCHEMA}}.agent_prompts CASCADE;
1
+ -- TEMPLATE: agent-prompts-migration.sql
2
+ -- PURPOSE: Creates the agent_prompts table for runtime-editable, versioned agent instructions.
3
+ -- READS STANDARD: ai-agents-prompts-in-db
4
+ -- PLACEHOLDERS:
5
+ -- {{SCHEMA}} — target schema (default: public)
6
+ -- LAST-VERIFIED: 2026-05-19
7
+
8
+ -- ============================================================
9
+ -- 1. TABLE
10
+ -- ============================================================
11
+
12
+ CREATE TABLE {{SCHEMA}}.agent_prompts (
13
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
14
+ agent_key text NOT NULL,
15
+ version int NOT NULL,
16
+ content text NOT NULL,
17
+ is_active boolean NOT NULL DEFAULT false,
18
+ created_at timestamptz NOT NULL DEFAULT now()
19
+ );
20
+
21
+ COMMENT ON TABLE {{SCHEMA}}.agent_prompts IS 'Runtime-editable, versioned system prompts for AI agents.';
22
+ COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.agent_key IS 'Logical identifier for the agent (e.g. ''proposal-agent'').';
23
+ COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.version IS 'Monotonically increasing per agent_key. Never reuse a version number.';
24
+ COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.content IS 'Immutable system-prompt text. UPDATE of this column is forbidden — insert a new row instead.';
25
+ COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.is_active IS 'Exactly one row per agent_key may have is_active = true (enforced by ix_agent_prompts_active).';
26
+ COMMENT ON COLUMN {{SCHEMA}}.agent_prompts.created_at IS 'Audit timestamp. Set at insert time, never updated.';
27
+
28
+ -- ============================================================
29
+ -- 2. INDEXES
30
+ -- ============================================================
31
+
32
+ -- Unique partial index: enforces at DB level that only one active prompt
33
+ -- exists per agent_key. Any attempt to INSERT a second active row for the
34
+ -- same key will fail with a unique-constraint violation.
35
+ CREATE UNIQUE INDEX ix_agent_prompts_active
36
+ ON {{SCHEMA}}.agent_prompts (agent_key)
37
+ WHERE is_active;
38
+
39
+ -- Regular index for the PromptRepository lookup (agent_key + is_active filter).
40
+ CREATE INDEX ix_agent_prompts_agent_key
41
+ ON {{SCHEMA}}.agent_prompts (agent_key);
42
+
43
+ -- Unique index: prevents duplicate version numbers per agent_key.
44
+ CREATE UNIQUE INDEX ix_agent_prompts_agent_key_version
45
+ ON {{SCHEMA}}.agent_prompts (agent_key, version);
46
+
47
+ -- ============================================================
48
+ -- 3. SEED (optional — remove if managed via backoffice)
49
+ -- ============================================================
50
+
51
+ -- INSERT INTO {{SCHEMA}}.agent_prompts (agent_key, version, content, is_active)
52
+ -- VALUES ('my-agent', 1, 'You are a helpful assistant.', true);
53
+
54
+ -- ============================================================
55
+ -- ROLLBACK
56
+ -- ============================================================
57
+
58
+ -- DROP TABLE IF EXISTS {{SCHEMA}}.agent_prompts CASCADE;
@@ -1,91 +1,91 @@
1
- -- TEMPLATE: pgvector-embeddings-migration.sql
2
- -- PURPOSE: Creates the kb_embeddings table for custom RAG — chunk storage with pgvector.
3
- -- READS STANDARD: ai-agents-rag-custom-pgvector
4
- -- PLACEHOLDERS:
5
- -- {{SCHEMA}} — target schema (default: public)
6
- -- LAST-VERIFIED: 2026-05-19
7
- --
8
- -- PREREQUISITES:
9
- -- - pgvector extension enabled in Neon (see infrastructure-neon-pgvector).
10
- -- - NpgsqlDataSourceBuilder.UseVector() called in the project's DB setup.
11
- --
12
- -- DIMENSION NOTE:
13
- -- embedding vector(1536) matches text-embedding-3-small (default 1536 dimensions).
14
- -- If the project uses a different embedding model, adjust the dimension to match.
15
- -- NEVER mix embedding models on a single column — vectors live in different spaces.
16
-
17
- -- ============================================================
18
- -- 1. EXTENSION
19
- -- ============================================================
20
-
21
- CREATE EXTENSION IF NOT EXISTS vector;
22
-
23
- -- ============================================================
24
- -- 2. TABLE
25
- -- ============================================================
26
-
27
- CREATE TABLE {{SCHEMA}}.kb_embeddings (
28
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
29
- tenant_id uuid NOT NULL,
30
- source text NOT NULL, -- document identifier or source URL
31
- chunk_index int NOT NULL, -- position of this chunk within the source document
32
- content text NOT NULL, -- chunk text returned to the agent as context
33
- embedding vector(1536) NOT NULL, -- vector dimension must match embedding model
34
- metadata jsonb NOT NULL DEFAULT '{}',
35
- created_at timestamptz NOT NULL DEFAULT now()
36
- );
37
-
38
- COMMENT ON TABLE {{SCHEMA}}.kb_embeddings IS 'RAG knowledge base — chunk text and pgvector embeddings for semantic search.';
39
- COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.tenant_id IS 'Tenant partition. Every query MUST filter on this column. Never query across tenants.';
40
- COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.source IS 'Document identifier or URL. Used for citation and re-indexing detection.';
41
- COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.chunk_index IS 'Zero-based position of this chunk within the source document.';
42
- COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.content IS 'Chunk text returned to the agent. Plain text, no markup.';
43
- COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.embedding IS 'Float vector. Dimension MUST match the embedding model declared in model-registry.json.';
44
- COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.metadata IS 'Arbitrary JSON metadata (section headers, page numbers, doc type, etc.). Used for filtering.';
45
-
46
- -- ============================================================
47
- -- 3. INDEXES
48
- -- ============================================================
49
-
50
- -- HNSW index for cosine similarity — production-grade recall, no training data required.
51
- -- vector_cosine_ops: use <=> operator (cosine distance, ascending = most similar first).
52
- -- m=16, ef_construction=64 are pgvector defaults. Tune per infrastructure-neon-pgvector §HNSW Parameters.
53
- CREATE INDEX ix_kb_embeddings_hnsw
54
- ON {{SCHEMA}}.kb_embeddings
55
- USING hnsw (embedding vector_cosine_ops)
56
- WITH (m = 16, ef_construction = 64);
57
-
58
- -- Index for the mandatory tenant_id filter (applied in every search query).
59
- CREATE INDEX ix_kb_embeddings_tenant
60
- ON {{SCHEMA}}.kb_embeddings (tenant_id);
61
-
62
- -- Composite index for source-level operations (re-indexing, deletion by document).
63
- CREATE INDEX ix_kb_embeddings_source
64
- ON {{SCHEMA}}.kb_embeddings (tenant_id, source);
65
-
66
- -- ============================================================
67
- -- 4. ROW-LEVEL SECURITY (multi-tenant)
68
- -- ============================================================
69
-
70
- -- RLS enforces tenant isolation at the database level as a second line of defence.
71
- -- The application already filters on tenant_id in every query; RLS is the backstop.
72
- --
73
- -- See templates/code/sql/postgresql-rls-policy.sql for the policy pattern that
74
- -- binds app.current_tenant_id (a session-level setting) to the RLS policy.
75
- --
76
- -- Uncomment and adapt once RLS is wired in the project:
77
- --
78
- -- ALTER TABLE {{SCHEMA}}.kb_embeddings ENABLE ROW LEVEL SECURITY;
79
- --
80
- -- CREATE POLICY kb_embeddings_tenant_isolation
81
- -- ON {{SCHEMA}}.kb_embeddings
82
- -- USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
83
-
84
- -- ============================================================
85
- -- ROLLBACK
86
- -- ============================================================
87
-
88
- -- DROP INDEX IF EXISTS {{SCHEMA}}.ix_kb_embeddings_hnsw;
89
- -- DROP INDEX IF EXISTS {{SCHEMA}}.ix_kb_embeddings_tenant;
90
- -- DROP INDEX IF EXISTS {{SCHEMA}}.ix_kb_embeddings_source;
91
- -- DROP TABLE IF EXISTS {{SCHEMA}}.kb_embeddings CASCADE;
1
+ -- TEMPLATE: pgvector-embeddings-migration.sql
2
+ -- PURPOSE: Creates the kb_embeddings table for custom RAG — chunk storage with pgvector.
3
+ -- READS STANDARD: ai-agents-rag-custom-pgvector
4
+ -- PLACEHOLDERS:
5
+ -- {{SCHEMA}} — target schema (default: public)
6
+ -- LAST-VERIFIED: 2026-05-19
7
+ --
8
+ -- PREREQUISITES:
9
+ -- - pgvector extension enabled in Neon (see infrastructure-neon-pgvector).
10
+ -- - NpgsqlDataSourceBuilder.UseVector() called in the project's DB setup.
11
+ --
12
+ -- DIMENSION NOTE:
13
+ -- embedding vector(1536) matches text-embedding-3-small (default 1536 dimensions).
14
+ -- If the project uses a different embedding model, adjust the dimension to match.
15
+ -- NEVER mix embedding models on a single column — vectors live in different spaces.
16
+
17
+ -- ============================================================
18
+ -- 1. EXTENSION
19
+ -- ============================================================
20
+
21
+ CREATE EXTENSION IF NOT EXISTS vector;
22
+
23
+ -- ============================================================
24
+ -- 2. TABLE
25
+ -- ============================================================
26
+
27
+ CREATE TABLE {{SCHEMA}}.kb_embeddings (
28
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
29
+ tenant_id uuid NOT NULL,
30
+ source text NOT NULL, -- document identifier or source URL
31
+ chunk_index int NOT NULL, -- position of this chunk within the source document
32
+ content text NOT NULL, -- chunk text returned to the agent as context
33
+ embedding vector(1536) NOT NULL, -- vector dimension must match embedding model
34
+ metadata jsonb NOT NULL DEFAULT '{}',
35
+ created_at timestamptz NOT NULL DEFAULT now()
36
+ );
37
+
38
+ COMMENT ON TABLE {{SCHEMA}}.kb_embeddings IS 'RAG knowledge base — chunk text and pgvector embeddings for semantic search.';
39
+ COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.tenant_id IS 'Tenant partition. Every query MUST filter on this column. Never query across tenants.';
40
+ COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.source IS 'Document identifier or URL. Used for citation and re-indexing detection.';
41
+ COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.chunk_index IS 'Zero-based position of this chunk within the source document.';
42
+ COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.content IS 'Chunk text returned to the agent. Plain text, no markup.';
43
+ COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.embedding IS 'Float vector. Dimension MUST match the embedding model declared in model-registry.json.';
44
+ COMMENT ON COLUMN {{SCHEMA}}.kb_embeddings.metadata IS 'Arbitrary JSON metadata (section headers, page numbers, doc type, etc.). Used for filtering.';
45
+
46
+ -- ============================================================
47
+ -- 3. INDEXES
48
+ -- ============================================================
49
+
50
+ -- HNSW index for cosine similarity — production-grade recall, no training data required.
51
+ -- vector_cosine_ops: use <=> operator (cosine distance, ascending = most similar first).
52
+ -- m=16, ef_construction=64 are pgvector defaults. Tune per infrastructure-neon-pgvector §HNSW Parameters.
53
+ CREATE INDEX ix_kb_embeddings_hnsw
54
+ ON {{SCHEMA}}.kb_embeddings
55
+ USING hnsw (embedding vector_cosine_ops)
56
+ WITH (m = 16, ef_construction = 64);
57
+
58
+ -- Index for the mandatory tenant_id filter (applied in every search query).
59
+ CREATE INDEX ix_kb_embeddings_tenant
60
+ ON {{SCHEMA}}.kb_embeddings (tenant_id);
61
+
62
+ -- Composite index for source-level operations (re-indexing, deletion by document).
63
+ CREATE INDEX ix_kb_embeddings_source
64
+ ON {{SCHEMA}}.kb_embeddings (tenant_id, source);
65
+
66
+ -- ============================================================
67
+ -- 4. ROW-LEVEL SECURITY (multi-tenant)
68
+ -- ============================================================
69
+
70
+ -- RLS enforces tenant isolation at the database level as a second line of defence.
71
+ -- The application already filters on tenant_id in every query; RLS is the backstop.
72
+ --
73
+ -- See templates/code/sql/postgresql-rls-policy.sql for the policy pattern that
74
+ -- binds app.current_tenant_id (a session-level setting) to the RLS policy.
75
+ --
76
+ -- Uncomment and adapt once RLS is wired in the project:
77
+ --
78
+ -- ALTER TABLE {{SCHEMA}}.kb_embeddings ENABLE ROW LEVEL SECURITY;
79
+ --
80
+ -- CREATE POLICY kb_embeddings_tenant_isolation
81
+ -- ON {{SCHEMA}}.kb_embeddings
82
+ -- USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
83
+
84
+ -- ============================================================
85
+ -- ROLLBACK
86
+ -- ============================================================
87
+
88
+ -- DROP INDEX IF EXISTS {{SCHEMA}}.ix_kb_embeddings_hnsw;
89
+ -- DROP INDEX IF EXISTS {{SCHEMA}}.ix_kb_embeddings_tenant;
90
+ -- DROP INDEX IF EXISTS {{SCHEMA}}.ix_kb_embeddings_source;
91
+ -- DROP TABLE IF EXISTS {{SCHEMA}}.kb_embeddings CASCADE;