@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.
- package/README.md +2 -2
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +3 -2
- package/framework/agents.json +1 -1
- package/framework/commands/commit.md +166 -166
- package/framework/commands/morph-infra.md +215 -215
- package/framework/commands/morph-preflight.md +205 -205
- package/framework/commands/morph-troubleshoot.md +124 -124
- package/framework/evals/scenarios/dotnet-crud.yaml +1 -1
- package/framework/hooks/claude-code/statusline.py +679 -679
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/activity-logger.js +160 -160
- package/framework/hooks/shared/hook-response.js +45 -45
- package/framework/hooks/shared/payload-utils.js +78 -78
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rubrics/backend.json +41 -41
- package/framework/rules/ai-agents-standards.md +45 -45
- package/framework/rules/dotnet-templates.md +86 -87
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +27 -27
- package/framework/rules/neon-standards.md +25 -25
- package/framework/rules/nextjs-standards.md +20 -20
- package/framework/rules/nextjs-templates.md +40 -40
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +9 -5
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +11 -6
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +13 -7
- package/framework/skills/core/morph-stack-scan/SKILL.md +5 -5
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +275 -275
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +34 -35
- package/framework/standards/backend/api/validation.md +2 -2
- package/framework/standards/backend/dotnet/scrutor.md +1 -3
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +42 -37
- package/framework/standards/data/nosql/cache/redis.md +2 -2
- package/framework/templates/CLAUDE.md.core.template +1 -1
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/README.md +0 -1
- package/framework/templates/REGISTRY.json +0 -21
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/abstractions/handler.cs +0 -3
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/constants/api-tags.cs +1 -1
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -282
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/handler.cs.hbs +23 -24
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/dotnet/jobs/job.cs +15 -15
- package/framework/templates/code/dotnet/test.cs +72 -101
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -113
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/index.js +13 -13
- package/src/commands/mcp/mcp-setup.js +519 -519
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/update.js +7 -4
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/LiveState.jsx +239 -239
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +41 -41
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/scanner/standards-scanner.js +26 -26
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/metrics-aggregator.js +56 -56
- package/src/lib/agents/metrics-calculator.js +125 -125
- package/src/lib/agents/output-collector.js +73 -73
- package/src/lib/detectors/claude-config-detector.js +152 -152
- package/src/lib/detectors/design-system-detector.js +231 -231
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +639 -639
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/index.js +13 -13
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +81 -81
- package/src/lib/validators/architecture/architecture-validator.js +259 -259
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/packages/package-validator.js +347 -347
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +349 -349
- package/src/scripts/setup-infra.js +383 -383
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/banner.js +52 -43
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/skills-installer.js +172 -172
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
- 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.
|
|
10
|
-
using
|
|
11
|
-
using {{pascalCase PROJECT_NAME}}.
|
|
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
|
|
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
|
|
27
|
-
private readonly
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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}}
|
|
44
|
-
var handler = new Create{{pascalCase FEATURE_NAME}}
|
|
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
|
|
61
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
78
|
+
var entity = CreateTestEntity("Test {{pascalCase FEATURE_NAME}}");
|
|
79
|
+
_db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
|
|
80
|
+
await _db.SaveChangesAsync();
|
|
96
81
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
114
|
-
var handler = new
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
|
151
|
-
|
|
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}}.
|
|
159
|
-
var handler = new Update{{pascalCase FEATURE_NAME}}
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
155
|
+
var entity = CreateTestEntity("To Delete");
|
|
156
|
+
_db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
|
|
157
|
+
await _db.SaveChangesAsync();
|
|
183
158
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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}}.
|
|
205
|
-
var handler = new Delete{{pascalCase FEATURE_NAME}}
|
|
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(
|
|
189
|
+
private static {{pascalCase FEATURE_NAME}} CreateTestEntity(string name)
|
|
223
190
|
{
|
|
224
|
-
return {{pascalCase FEATURE_NAME}}
|
|
225
|
-
|
|
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}}
|
|
248
|
-
var command = new Create{{pascalCase FEATURE_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}}
|
|
266
|
-
var command = new Create{{pascalCase FEATURE_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}}.
|
|
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}}
|
|
281
|
-
var command = new Create{{pascalCase FEATURE_NAME}}
|
|
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}}.
|
|
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}}
|
|
300
|
-
var command = new Update{{pascalCase FEATURE_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}}
|
|
314
|
-
var command = new Update{{pascalCase FEATURE_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}}.
|
|
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;
|