@polymorphism-tech/morph-spec 8.45.0 → 8.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CLAUDE.md +6 -5
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +2 -4
  4. package/bin/validate.js +4 -25
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -2
  9. package/framework/MORPH.md +9 -9
  10. package/framework/commands/create-umbraco-lp.md +2 -0
  11. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +1 -1
  14. package/framework/commands/morph-infra.md +17 -203
  15. package/framework/commands/morph-media.md +3 -0
  16. package/framework/commands/morph-preflight.md +5 -28
  17. package/framework/commands/morph-proposal.md +2 -2
  18. package/framework/commands/morph-troubleshoot.md +2 -14
  19. package/framework/docs/CLI.md +8 -7
  20. package/framework/hooks/claude-code/core/state-sync.js +13 -1
  21. package/framework/hooks/shared/e2e-check.js +3 -4
  22. package/framework/hooks/shared/state-writer.js +1 -0
  23. package/framework/rules/ai-agents-standards.md +18 -16
  24. package/framework/rules/asaas-standards.md +3 -1
  25. package/framework/rules/csharp-standards.md +10 -8
  26. package/framework/rules/data-standards.md +5 -5
  27. package/framework/rules/dotnet-templates.md +1 -1
  28. package/framework/rules/frontend-standards.md +4 -2
  29. package/framework/rules/gsap-standards.md +10 -5
  30. package/framework/rules/hangfire-standards.md +3 -1
  31. package/framework/rules/infrastructure-standards.md +4 -2
  32. package/framework/rules/morph-workflow.md +2 -2
  33. package/framework/rules/neon-auth-standards.md +3 -1
  34. package/framework/rules/neon-standards.md +6 -4
  35. package/framework/rules/nextjs-standards.md +13 -11
  36. package/framework/rules/nextjs-templates.md +1 -1
  37. package/framework/rules/resend-standards.md +3 -1
  38. package/framework/schemas/config.schema.json +4 -0
  39. package/framework/schemas/feature.schema.json +4 -4
  40. package/framework/skills/README.md +1 -1
  41. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  42. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  43. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  44. package/framework/skills/core/morph-brainstorming/SKILL.md +3 -3
  45. package/framework/skills/core/morph-hotfix/SKILL.md +4 -4
  46. package/framework/skills/core/morph-implement/SKILL.md +3 -3
  47. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  48. package/framework/skills/core/morph-media/SKILL.md +3 -3
  49. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  50. package/framework/skills/core/morph-proposal/SKILL.md +3 -3
  51. package/framework/skills/core/morph-review/SKILL.md +30 -17
  52. package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
  53. package/framework/skills/core/morph-standards/SKILL.md +6 -6
  54. package/framework/skills/core/morph-uiux/SKILL.md +3 -3
  55. package/framework/standards/STANDARDS.json +59 -56
  56. package/framework/standards/ai-agents/evals-with-cache.md +1 -1
  57. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  58. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  59. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  60. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  61. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  62. package/framework/standards/ai-agents/setup.md +1 -1
  63. package/framework/standards/ai-agents/structured-output.md +1 -1
  64. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  65. package/framework/standards/architecture/code-comments.md +2 -2
  66. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  67. package/framework/standards/backend/api/validation.md +135 -47
  68. package/framework/standards/backend/database/postgresql/database.md +11 -7
  69. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  70. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  71. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  72. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  73. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  74. package/framework/standards/backend/dotnet/testing.md +1 -1
  75. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  76. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  77. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  78. package/framework/standards/data/nosql/cache/redis.md +120 -44
  79. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  80. package/framework/standards/frontend/design-system/animations.md +1 -1
  81. package/framework/standards/frontend/design-system/naming.md +1 -1
  82. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  83. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  84. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  85. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  86. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  87. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  88. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  89. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  90. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  91. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  92. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  93. package/framework/templates/infrastructure/github/README.md +2 -1
  94. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  95. package/package.json +1 -1
  96. package/src/commands/observability/telemetry.js +3 -5
  97. package/src/commands/project/init.js +2 -1
  98. package/src/commands/project/update.js +16 -18
  99. package/src/commands/state/approve.js +17 -0
  100. package/src/commands/state/gate-check.js +4 -14
  101. package/src/commands/validation/verify.js +99 -57
  102. package/src/core/paths/output-schema.js +1 -1
  103. package/src/dashboard/app/App.jsx +0 -2
  104. package/src/dashboard/app/components/CommandPalette.jsx +2 -3
  105. package/src/dashboard/app/components/EntityLink.jsx +0 -1
  106. package/src/dashboard/app/components/Markdown.jsx +1 -1
  107. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  108. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  109. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  110. package/src/dashboard/scanner/activity-scanner.js +1 -1
  111. package/src/dashboard/scanner/docs-scanner.js +5 -23
  112. package/src/dashboard/scanner/features-scanner.js +1 -1
  113. package/src/dashboard/scanner/index.js +0 -2
  114. package/src/dashboard/scanner/rules-scanner.js +15 -10
  115. package/src/dashboard/scanner/skills-scanner.js +2 -3
  116. package/src/lib/ci/ci-verify.js +118 -0
  117. package/src/lib/ci/github-ci.js +192 -0
  118. package/src/lib/ci/workflow-scaffold.js +122 -0
  119. package/src/lib/evaluator-report.js +13 -0
  120. package/src/lib/risk-detector.js +8 -0
  121. package/src/lib/stack/stack-profile.js +1 -1
  122. package/src/lib/telemetry/verify-event.js +3 -2
  123. package/src/lib/validators/reuse/reuse-validator.js +2 -3
  124. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  125. package/src/lib/validators/validation-runner.js +8 -88
  126. package/src/lib/validators/verify-runner.js +22 -72
  127. package/src/scripts/setup-infra.js +33 -22
  128. package/src/utils/agents-installer.js +20 -8
  129. package/src/utils/file-copier.js +7 -5
  130. package/framework/commands/commit.md +0 -166
  131. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  132. package/framework/rubrics/backend.json +0 -41
  133. package/framework/rubrics/default.json +0 -36
  134. package/framework/rubrics/frontend.json +0 -36
  135. package/framework/templates/project/validate.js +0 -97
  136. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  137. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  138. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  139. package/src/lib/validators/architecture/index.js +0 -7
  140. package/src/lib/validators/css/css-validator.js +0 -340
  141. package/src/lib/validators/css/index.js +0 -7
  142. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  143. package/src/lib/validators/design-system/index.js +0 -7
  144. package/src/lib/validators/score-aggregator.js +0 -244
  145. package/src/lib/validators/ui/index.js +0 -7
  146. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** coolify, docker, deploy, deployment, container, vps, ssh
6
6
  > **Load When:** coolify or docker deployment keywords detected
7
7
 
8
- **Verified against:** .NET 10 + Next.js 15 + Node 22 (Docker base images). Last-verified: 2026-07-07.
8
+ **Verified against:** .NET 10 + Next.js 15 + Node 22 (Docker base images) + Coolify 4.x API. The `POST /api/v1/deploy` contract was checked against the Coolify docs/OpenAPI on 2026-10-03, and Coolify 4.3.23 was observed answering `GET /deploy` with "This endpoint has changed to a POST request"; Dockerfiles and snippets are documentary. Last-verified: 2026-10-03.
9
9
 
10
10
  ---
11
11
 
@@ -17,14 +17,14 @@ Stack: Next.js 15 + Neon PostgreSQL + .NET Backend
17
17
 
18
18
  Coolify is Polymorphism Tech's standard self-hosted deployment platform for Docker workloads.
19
19
 
20
- There is no MCP server or CLI tool for Coolify. All access is via **SSH into the VPS**, from which the Coolify API is reached at `http://localhost:8000/api/v1/` (it is not exposed publicly). The API token lives server-side only (e.g. `~/.coolify-token`, `chmod 600`) -- never print it, never commit it, never let it leave the SSH session.
20
+ We do not use a Coolify MCP server or CLI. All access is via **SSH into the VPS**, from which the Coolify API is reached at `http://localhost:8000/api/v1/`: keep the VPS firewall to SSH and HTTP(S) only, so port 8000 never answers from outside. The API token lives server-side only (a `chmod 600` file in the SSH user's home, written here as `~/<token-file>`) -- read it inside the remote command, never print it, never commit it, never let it leave the SSH session.
21
21
 
22
22
  ```bash
23
23
  # Canonical access pattern — token is read and used entirely server-side
24
- ssh <vps-ssh-alias> 'T=$(cat ~/.coolify-token); curl -s -H "Authorization: Bearer $T" http://localhost:8000/api/v1/projects'
24
+ ssh <vps-ssh-alias> 'T=$(cat ~/<token-file>); curl -s -H "Authorization: Bearer $T" http://localhost:8000/api/v1/projects'
25
25
  ```
26
26
 
27
- Useful endpoints: `/version`, `/projects`, `/servers`, `/applications`, `/deploy?uuid={uuid}`. Changing a domain: `PATCH /applications/{uuid}` with `{"domains":"https://x,https://www.x"}`, then `GET /deploy?uuid={uuid}` to redeploy.
27
+ Useful endpoints: `GET /version`, `GET /projects`, `GET /servers`, `GET /applications`, `POST /deploy?uuid={uuid}`. Changing a domain: `PATCH /applications/{uuid}` with `{"domains":"https://x,https://www.x"}` (a `dockercompose` application refuses `domains` and takes `docker_compose_domains: [{"name":"<service>","domain":"https://x:<port>"}]`), then `POST /deploy?uuid={uuid}` to redeploy. The deploy trigger is **POST**: Coolify 4.3.23 answers a `GET /deploy` with "This endpoint has changed to a POST request" and deploys nothing, so a script that only greps for an error status reports a deploy that never happened.
28
28
 
29
29
  ## Core Rules
30
30
 
@@ -202,13 +202,15 @@ const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });
202
202
 
203
203
  ## Triggering a Deploy via SSH + API
204
204
 
205
- Since there is no MCP server or CLI for Coolify, every deploy operation (manual redeploy, CI/CD, or troubleshooting) goes through the same SSH + curl pattern used for all other Coolify access:
205
+ Every deploy operation (manual redeploy, CI/CD, or troubleshooting) goes through the same SSH + curl pattern used for all other Coolify access:
206
206
 
207
207
  ```bash
208
208
  # Redeploy an existing application after a new image/commit is ready
209
- ssh <vps-ssh-alias> 'T=$(cat ~/.coolify-token); curl -s -X GET -H "Authorization: Bearer $T" "http://localhost:8000/api/v1/deploy?uuid=<application-uuid>"'
209
+ ssh <vps-ssh-alias> 'T=$(cat ~/<token-file>); curl -s -X POST -H "Authorization: Bearer $T" "http://localhost:8000/api/v1/deploy?uuid=<application-uuid>"'
210
210
  ```
211
211
 
212
+ A queued deploy answers `{"deployments":[{"message":...,"resource_uuid":...,"deployment_uuid":...}]}`; treat a response without `deployment_uuid` as "nothing was deployed". `force=true` rebuilds without the build cache.
213
+
212
214
  In GitHub Actions this becomes an SSH step using a deploy key stored as a secret (see the `github-workflow-deploy-coolify` template) -- never a direct HTTP call to the Coolify API from the runner, since the API port is not reachable outside the VPS.
213
215
 
214
216
  ## Deployment Checklist
@@ -5,20 +5,22 @@
5
5
  > **Keywords:** pgvector, vector, embeddings, similarity search, HNSW, cosine
6
6
  > **Load When:** pgvector or embedding keywords detected
7
7
 
8
- **Verified against:** .NET 10 + Pgvector.EntityFrameworkCore + pgvector (HNSW). Last-verified: 2026-05-20.
8
+ **Verified against:** .NET 10 (SDK 10.0.400-preview) + Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 + Pgvector.EntityFrameworkCore 0.3.0 + Microsoft.Extensions.AI 10.9.0 (ai-pin 2026-09-08). The entity, configuration, DbContext, search-handler and raw-Npgsql blocks compile with `TreatWarningsAsErrors` (handler over the `Result`/`IHandler` of `result-pattern.md`/`vertical-slice.md`), and the handler's query shape was translated with `ToQueryString()` to `ORDER BY d.embedding <=> @query`; the SQL (index DDL, `ef_search`, the two SQL functions) is documentary, checked against the pgvector README. Last-verified: 2026-10-03.
9
9
 
10
10
  ---
11
11
 
12
- Stack: .NET VSA + Neon Serverless Postgres
12
+ Stack: .NET VSA + Neon Serverless Postgres. For the end-to-end RAG pipeline (ingestion, retrieval, generation) on top of this mapping, see `backend/database/vector-search-rag.md`.
13
13
 
14
14
  ## Core Rules
15
15
 
16
16
  - ALWAYS use HNSW indexes for production (faster queries, no training data required)
17
17
  - ALWAYS match vector dimensions to your embedding model (e.g., 1536 for text-embedding-3-small)
18
18
  - NEVER store embeddings without an index -- full table scan at query time
19
- - ALWAYS call `UseVector()` on `NpgsqlDataSourceBuilder` before using vector types
20
- - ALWAYS register the `vector` extension in `OnModelCreating`
21
- - Use `ReadOnlyMemory<float>` for vector properties in EF Core entities
19
+ - ALWAYS call `UseVector()` on the `UseNpgsql(...)` options builder (EF Core); `NpgsqlDataSourceBuilder.UseVector()` is only for raw Npgsql
20
+ - ALWAYS register the `vector` extension in `OnModelCreating` (`HasPostgresExtension("vector")`)
21
+ - Map vector properties as `Pgvector.Vector` -- EF Core does not map `float[]` or `ReadOnlyMemory<float>` to the `vector` column
22
+ - Query through the `ApplicationDbContext` in the slice handler with the `CosineDistance`/`L2Distance`/`MaxInnerProduct` LINQ translations -- no repository class, no raw SQL from C# without a `decisions.md` justification
23
+ - IDs come from the app (`Guid.CreateVersion7()`), never from a `gen_random_uuid()` column default
22
24
 
23
25
  ## Enable pgvector
24
26
 
@@ -26,54 +28,56 @@ Stack: .NET VSA + Neon Serverless Postgres
26
28
  CREATE EXTENSION IF NOT EXISTS vector;
27
29
  ```
28
30
 
31
+ `HasPostgresExtension("vector")` in `OnModelCreating` makes the EF Core migration emit this statement, so a hand-written migration is not needed.
32
+
29
33
  ## EF Core Entity Mapping
30
34
 
31
- ### NuGet Package
35
+ ### NuGet Packages
32
36
 
33
- ```bash
34
- dotnet add package Pgvector.EntityFrameworkCore
37
+ ```xml
38
+ <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
39
+ <PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
35
40
  ```
36
41
 
42
+ `Pgvector.EntityFrameworkCore` 0.3.0 requires `Npgsql.EntityFrameworkCore.PostgreSQL` >= 9.0.1 and brings `Pgvector` (the `Vector` type and the raw-Npgsql plugin) transitively. The configuration below maps each column to the snake_case names of `backend/database/postgresql/database.md` (`embedding`, `created_at`) with `HasColumnName`; without it EF creates `"Embedding"`, `"CreatedAt"` and every SQL snippet below would have to quote PascalCase names. (A project that wants this for every entity can add `EFCore.NamingConventions` and `UseSnakeCaseNamingConvention()` once, instead of per column -- a project-wide decision for `decisions.md`.)
43
+
37
44
  ### Entity Definition
38
45
 
39
46
  ```csharp
40
- using System.ComponentModel.DataAnnotations.Schema;
41
47
  using Pgvector;
42
48
 
43
- public class Document
49
+ public sealed class Document
44
50
  {
45
51
  public Guid Id { get; set; }
46
52
  public string Title { get; set; } = string.Empty;
47
53
  public string Content { get; set; } = string.Empty;
48
54
  public string? Metadata { get; set; }
49
-
50
- [Column(TypeName = "vector(1536)")]
51
55
  public Vector? Embedding { get; set; }
52
-
53
- public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
56
+ public DateTimeOffset CreatedAt { get; set; }
54
57
  }
55
58
  ```
56
59
 
60
+ `Id` and `CreatedAt` are set by the handler that creates the row (`Guid.CreateVersion7()` and `TimeProvider.GetUtcNow()`), not by property initializers or column defaults. `Embedding` is nullable because a row can exist before its embedding is generated.
61
+
57
62
  ### IEntityTypeConfiguration
58
63
 
59
64
  ```csharp
60
65
  using Microsoft.EntityFrameworkCore;
61
66
  using Microsoft.EntityFrameworkCore.Metadata.Builders;
62
67
 
63
- public class DocumentConfiguration : IEntityTypeConfiguration<Document>
68
+ public sealed class DocumentConfiguration : IEntityTypeConfiguration<Document>
64
69
  {
65
70
  public void Configure(EntityTypeBuilder<Document> builder)
66
71
  {
67
72
  builder.ToTable("documents");
68
73
  builder.HasKey(d => d.Id);
69
- builder.Property(d => d.Id).HasDefaultValueSql("gen_random_uuid()");
70
- builder.Property(d => d.Title).IsRequired();
71
- builder.Property(d => d.Content).IsRequired();
72
- builder.Property(d => d.Metadata).HasColumnType("jsonb");
73
- builder.Property(d => d.Embedding).HasColumnType("vector(1536)");
74
- builder.Property(d => d.CreatedAt).HasDefaultValueSql("now()");
75
-
76
- // HNSW index for cosine similarity
74
+ builder.Property(d => d.Id).HasColumnName("id").ValueGeneratedNever();
75
+ builder.Property(d => d.Title).HasColumnName("title").IsRequired();
76
+ builder.Property(d => d.Content).HasColumnName("content").IsRequired();
77
+ builder.Property(d => d.Metadata).HasColumnName("metadata").HasColumnType("jsonb");
78
+ builder.Property(d => d.Embedding).HasColumnName("embedding").HasColumnType("vector(1536)");
79
+ builder.Property(d => d.CreatedAt).HasColumnName("created_at");
80
+
77
81
  builder.HasIndex(d => d.Embedding)
78
82
  .HasMethod("hnsw")
79
83
  .HasOperators("vector_cosine_ops")
@@ -86,12 +90,12 @@ public class DocumentConfiguration : IEntityTypeConfiguration<Document>
86
90
  ### DbContext Setup
87
91
 
88
92
  ```csharp
89
- public class AppDbContext : DbContext
93
+ using Microsoft.EntityFrameworkCore;
94
+
95
+ public sealed class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
90
96
  {
91
97
  public DbSet<Document> Documents => Set<Document>();
92
98
 
93
- public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
94
-
95
99
  protected override void OnModelCreating(ModelBuilder modelBuilder)
96
100
  {
97
101
  modelBuilder.HasPostgresExtension("vector");
@@ -99,13 +103,22 @@ public class AppDbContext : DbContext
99
103
  }
100
104
  }
101
105
 
102
- // Registration with vector support
103
- builder.Services.AddDbContext<AppDbContext>(options =>
104
- options.UseNpgsql(connectionString, npgsql => npgsql.UseVector()));
106
+ public static class VectorDatabaseRegistration
107
+ {
108
+ public static IServiceCollection AddVectorDatabase(this IServiceCollection services, IConfiguration configuration) =>
109
+ services.AddDbContext<ApplicationDbContext>(options =>
110
+ options.UseNpgsql(
111
+ configuration.GetConnectionString("DefaultConnection"),
112
+ npgsql => npgsql.UseVector()));
113
+ }
105
114
  ```
106
115
 
116
+ The connection string comes from configuration (user-secrets in development, environment variables in Coolify) -- see `infrastructure/neon/neon-setup.md`.
117
+
107
118
  ## HNSW Index Creation (Raw SQL)
108
119
 
120
+ The EF configuration above generates the cosine index. The SQL equivalents, for reference and for hand-written migrations:
121
+
109
122
  ```sql
110
123
  -- Cosine distance (most common for normalized embeddings)
111
124
  CREATE INDEX idx_documents_embedding ON documents
@@ -115,10 +128,15 @@ CREATE INDEX idx_documents_embedding ON documents
115
128
  CREATE INDEX idx_documents_embedding_l2 ON documents
116
129
  USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
117
130
 
118
- -- Tune search accuracy per session
119
- SET hnsw.ef_search = 100;
131
+ -- Tune search accuracy for the current transaction
132
+ BEGIN;
133
+ SET LOCAL hnsw.ef_search = 100;
134
+ -- ... similarity query ...
135
+ COMMIT;
120
136
  ```
121
137
 
138
+ On the Neon pooled (`-pooler`, PgBouncer transaction mode) endpoint a plain `SET` does not stick to your next statement: use `SET LOCAL` inside the same transaction as the query, or set it per role/database.
139
+
122
140
  ### HNSW Parameters
123
141
 
124
142
  | Parameter | Default | Tuning |
@@ -129,15 +147,58 @@ SET hnsw.ef_search = 100;
129
147
 
130
148
  ## Distance Operators
131
149
 
132
- | Operator | Function | Index Ops | Use Case |
133
- |----------|----------|-----------|----------|
134
- | `<=>` | Cosine distance | `vector_cosine_ops` | Normalized embeddings (most common) |
135
- | `<->` | L2 (Euclidean) | `vector_l2_ops` | Spatial/positional data |
136
- | `<#>` | Inner product (neg) | `vector_ip_ops` | Pre-normalized, max similarity |
150
+ | Operator | LINQ (Pgvector.EntityFrameworkCore) | Index Ops | Use Case |
151
+ |----------|-------------------------------------|-----------|----------|
152
+ | `<=>` | `CosineDistance` | `vector_cosine_ops` | Normalized embeddings (most common) |
153
+ | `<->` | `L2Distance` | `vector_l2_ops` | Spatial/positional data |
154
+ | `<#>` | `MaxInnerProduct` (negative inner product) | `vector_ip_ops` | Pre-normalized, max similarity |
155
+
156
+ The index operator class must match the function used in the query, or Postgres ignores the index and falls back to a sequential scan.
137
157
 
138
158
  ## Similarity Search
139
159
 
140
- ### SQL Function
160
+ ### Search handler (EF Core in the slice)
161
+
162
+ ```csharp
163
+ // Features/DocumentFeature/SearchDocuments/SearchDocumentsHandler.cs
164
+ using Microsoft.EntityFrameworkCore;
165
+ using Microsoft.Extensions.AI;
166
+ using Pgvector;
167
+ using Pgvector.EntityFrameworkCore;
168
+
169
+ public sealed record SearchDocumentsRequest(string Query, int Limit = 10, double MinSimilarity = 0.78);
170
+ public sealed record DocumentMatch(Guid Id, string Title, string Content, double Similarity);
171
+
172
+ public sealed class SearchDocumentsHandler(
173
+ ApplicationDbContext db,
174
+ IEmbeddingGenerator<string, Embedding<float>> embeddings)
175
+ : IHandler<SearchDocumentsRequest, Result<IReadOnlyList<DocumentMatch>>>
176
+ {
177
+ public async Task<Result<IReadOnlyList<DocumentMatch>>> HandleAsync(
178
+ SearchDocumentsRequest request, CancellationToken cancellationToken)
179
+ {
180
+ var query = new Vector(await embeddings.GenerateVectorAsync(request.Query, cancellationToken: cancellationToken));
181
+ var maxDistance = 1 - request.MinSimilarity;
182
+
183
+ var matches = await db.Documents
184
+ .Where(d => d.Embedding != null)
185
+ .Select(d => new { d.Id, d.Title, d.Content, Distance = d.Embedding!.CosineDistance(query) })
186
+ .Where(x => x.Distance <= maxDistance)
187
+ .OrderBy(x => x.Distance)
188
+ .Take(request.Limit)
189
+ .Select(x => new DocumentMatch(x.Id, x.Title, x.Content, 1 - x.Distance))
190
+ .ToListAsync(cancellationToken);
191
+
192
+ return Result.Success<IReadOnlyList<DocumentMatch>>(matches);
193
+ }
194
+ }
195
+ ```
196
+
197
+ Storing an embedding is an ordinary EF write in the handler that owns the document: `document.Embedding = new Vector(await embeddings.GenerateVectorAsync(text, cancellationToken: ct));` followed by `SaveChangesAsync`. No `UPDATE ... SET embedding` from C#.
198
+
199
+ ### SQL Functions (only with a `decisions.md` justification)
200
+
201
+ When the query must live in the database (called from several apps, or a hybrid score the LINQ translation cannot express), a SQL function is acceptable -- record why in `decisions.md`, call it through EF Core with interpolated parameters, and keep the same distance rules:
141
202
 
142
203
  ```sql
143
204
  CREATE OR REPLACE FUNCTION match_documents(
@@ -155,40 +216,10 @@ LANGUAGE sql STABLE AS $$
155
216
  $$;
156
217
  ```
157
218
 
158
- ### C# Repository
159
-
160
- ```csharp
161
- using Pgvector;
162
-
163
- public sealed class DocumentRepository(AppDbContext db)
164
- {
165
- public async Task StoreEmbeddingAsync(
166
- Guid documentId, float[] embedding, CancellationToken ct = default)
167
- {
168
- await db.Database.ExecuteSqlInterpolatedAsync(
169
- $"UPDATE documents SET embedding = {new Vector(embedding)} WHERE id = {documentId}", ct);
170
- }
171
-
172
- public async Task<List<DocumentMatch>> SearchSimilarAsync(
173
- float[] queryEmbedding, int limit = 10, float threshold = 0.78f,
174
- CancellationToken ct = default)
175
- {
176
- return await db.Database.SqlQuery<DocumentMatch>($"""
177
- SELECT id, title, content,
178
- 1 - (embedding <=> {new Vector(queryEmbedding)}::vector) AS similarity
179
- FROM documents
180
- WHERE 1 - (embedding <=> {new Vector(queryEmbedding)}::vector) > {threshold}
181
- ORDER BY embedding <=> {new Vector(queryEmbedding)}::vector
182
- LIMIT {limit}
183
- """).ToListAsync(ct);
184
- }
185
- }
186
-
187
- public record DocumentMatch(Guid Id, string Title, string Content, float Similarity);
188
- ```
189
-
190
219
  ## Hybrid Search (Vector + Full-Text)
191
220
 
221
+ Blends the vector score with `ts_rank`. Same rule as above: a SQL function, justified in `decisions.md`.
222
+
192
223
  ```sql
193
224
  CREATE OR REPLACE FUNCTION hybrid_search(
194
225
  query_text text,
@@ -218,14 +249,25 @@ LANGUAGE sql STABLE AS $$
218
249
  $$;
219
250
  ```
220
251
 
252
+ For Portuguese content use the `'portuguese'` text search configuration in both `to_tsvector` and `plainto_tsquery`.
253
+
221
254
  ## Npgsql Vector Type Handler
222
255
 
223
- The `Pgvector.EntityFrameworkCore` package automatically registers the vector type handler when you call `UseVector()`. For raw Npgsql (without EF Core):
256
+ With EF Core, `npgsql.UseVector()` inside `UseNpgsql(...)` registers the type mapping and the LINQ translations. For raw Npgsql (without EF Core), register the plugin on the data source:
224
257
 
225
258
  ```csharp
226
- var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
227
- dataSourceBuilder.UseVector();
228
- await using var dataSource = dataSourceBuilder.Build();
259
+ using Npgsql;
260
+ using Pgvector.Npgsql;
261
+
262
+ public static class RawVectorDataSource
263
+ {
264
+ public static NpgsqlDataSource Build(string connectionString)
265
+ {
266
+ var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
267
+ dataSourceBuilder.UseVector();
268
+ return dataSourceBuilder.Build();
269
+ }
270
+ }
229
271
  ```
230
272
 
231
273
  ## Common Mistakes
@@ -234,7 +276,10 @@ await using var dataSource = dataSourceBuilder.Build();
234
276
  |-------|-------|-----|
235
277
  | No index on embedding column | HNSW index | Full table scan, extremely slow |
236
278
  | `ORDER BY similarity DESC` | `ORDER BY embedding <=> query ASC` | Operators return distance, not similarity |
237
- | Missing `UseVector()` | Call on `NpgsqlDataSourceBuilder` | Npgsql cannot map vector types without it |
279
+ | Missing `UseVector()` | `npgsql.UseVector()` in `UseNpgsql(...)` | Npgsql cannot map vector types without it |
238
280
  | Mixing embedding dimensions | Consistent dimensions per column | Dimension mismatch causes runtime errors |
239
- | `float[]` for EF Core property | `Vector` from Pgvector package | EF Core mapping requires the Pgvector type |
281
+ | `float[]` / `ReadOnlyMemory<float>` for the EF property | `Vector` from the Pgvector package | EF Core mapping requires the Pgvector type |
240
282
  | No `HasPostgresExtension("vector")` | Add in `OnModelCreating` | Extension must be declared for migrations |
283
+ | A `DocumentRepository` with `ExecuteSqlInterpolatedAsync`/`SqlQuery` | LINQ `CosineDistance` in the slice handler | `DbSet<T>` is already the repository; raw SQL bypasses the translation that keeps the index in use |
284
+ | `SET hnsw.ef_search` on the pooled endpoint | `SET LOCAL` inside the query's transaction | PgBouncer transaction mode hands the next statement another server session |
285
+ | `HasDefaultValueSql("gen_random_uuid()")` on the id | `Id = Guid.CreateVersion7()` in the handler | App-generated, time-ordered ids |
@@ -1,11 +1,11 @@
1
1
  # Neon Serverless Postgres Standard
2
2
 
3
3
  > **Scope:** neon
4
- > **Layer:** 0 (always load)
4
+ > **Layer:** 0 (resumo injetado pela rule `neon-standards` em `**/neon/**`, `**/database/**` e migrations `.sql`; texto completo sob demanda via `morph-spec standards --find`)
5
5
  > **Keywords:** neon, serverless postgres, branching, scale-to-zero, connection string, neonctl
6
6
  > **Load When:** neon stack detected
7
7
 
8
- **Verified against:** .NET 10 + Npgsql/EF Core 10 + neonctl. Last-verified: 2026-05-20.
8
+ **Verified against:** .NET 10 (SDK 10.0.400-preview) + Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 + neonctl. The DbContext registration, design-time factory and cold-start blocks compile with `TreatWarningsAsErrors`; that Npgsql 10 rejects `postgresql://` URIs and marks `Trust Server Certificate` as "no longer needed and does nothing" was measured on 10.0.3; the suspend-timeout API and `--cu` ranges were checked against the Neon docs (context7). neonctl commands are documentary. Last-verified: 2026-10-03.
9
9
 
10
10
  ---
11
11
 
@@ -15,7 +15,7 @@ Stack: .NET + Neon Serverless Postgres
15
15
 
16
16
  - ALWAYS use pooled connection strings (`-pooler` endpoint) for application traffic
17
17
  - ALWAYS use direct connection strings for migrations and schema changes
18
- - NEVER hardcode connection strings -- use environment variables
18
+ - NEVER hardcode connection strings or commit them in `appsettings*.json` -- user-secrets in development, environment variables in Coolify
19
19
  - ALWAYS enable SSL (`SSL Mode=Require`) for all connections
20
20
  - ALWAYS configure retry policies for cold-start resilience
21
21
  - NEVER leave branches without expiration -- set TTL for dev/CI branches
@@ -42,38 +42,49 @@ neonctl projects list
42
42
  | Pooled | `ep-xxxx-pooler.region.aws.neon.tech` | Application queries, high concurrency |
43
43
  | Direct | `ep-xxxx.region.aws.neon.tech` | Migrations, EF Core `dotnet ef`, advisory locks |
44
44
 
45
- ### Environment Variables
45
+ ### Configuration Keys
46
46
 
47
- | Variable | Purpose |
48
- |----------|---------|
49
- | `DATABASE_URL` | Pooled connection for application runtime |
50
- | `DATABASE_URL_UNPOOLED` | Direct connection for migrations and CLI tools |
47
+ The .NET app reads two connection strings through `IConfiguration`. Neon's integrations call them `DATABASE_URL` / `DATABASE_URL_UNPOOLED`; in .NET they are:
51
48
 
52
- ```ini
53
- # Pooled (application)
54
- DATABASE_URL="Host=ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech;Database=mydb;Username=myuser;Password=secret;SSL Mode=Require"
49
+ | .NET key (env var form) | Neon name | Purpose |
50
+ |-------------------------|-----------|---------|
51
+ | `ConnectionStrings:DefaultConnection` (`ConnectionStrings__DefaultConnection`) | `DATABASE_URL` | Pooled connection for application runtime |
52
+ | `ConnectionStrings:MigrationConnection` (`ConnectionStrings__MigrationConnection`) | `DATABASE_URL_UNPOOLED` | Direct connection for migrations and CLI tools |
55
53
 
56
- # Direct (migrations)
57
- DATABASE_URL_UNPOOLED="Host=ep-cool-darkness-123456.us-east-2.aws.neon.tech;Database=mydb;Username=myuser;Password=secret;SSL Mode=Require"
54
+ Npgsql only parses the **keyword** format (`Host=...;Database=...`). The `postgresql://user:pass@host/db?sslmode=require` URI that the Neon console shows by default throws `ArgumentException` in `NpgsqlConnectionStringBuilder` (measured on Npgsql 10.0.3): copy the .NET form from the console's connection dialog, or convert it.
55
+
56
+ ```bash
57
+ # Development: user-secrets (stored outside the repo, per user)
58
+ dotnet user-secrets init
59
+ dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=<endpoint-id>-pooler.<region>.aws.neon.tech;Database=<db>;Username=<role>;Password=<password>;SSL Mode=Require"
60
+ dotnet user-secrets set "ConnectionStrings:MigrationConnection" "Host=<endpoint-id>.<region>.aws.neon.tech;Database=<db>;Username=<role>;Password=<password>;SSL Mode=Require"
61
+
62
+ # Production (Coolify environment variables, set in the UI or API -- never in the Dockerfile)
63
+ # ConnectionStrings__DefaultConnection=Host=<endpoint-id>-pooler.<region>.aws.neon.tech;...;SSL Mode=Require
64
+ # ConnectionStrings__MigrationConnection=Host=<endpoint-id>.<region>.aws.neon.tech;...;SSL Mode=Require
58
65
  ```
59
66
 
60
67
  ## EF Core Configuration
61
68
 
62
69
  ### appsettings.json
63
70
 
71
+ `appsettings.json` is committed, so it carries **no** connection string with a password -- the keys come from user-secrets / environment variables, which override it:
72
+
64
73
  ```json
65
74
  {
66
75
  "ConnectionStrings": {
67
- "DefaultConnection": "Host=ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech;Database=mydb;Username=myuser;Password=secret;SSL Mode=Require;Trust Server Certificate=true",
68
- "MigrationConnection": "Host=ep-cool-darkness-123456.us-east-2.aws.neon.tech;Database=mydb;Username=myuser;Password=secret;SSL Mode=Require;Trust Server Certificate=true"
76
+ "DefaultConnection": "",
77
+ "MigrationConnection": ""
69
78
  }
70
79
  }
71
80
  ```
72
81
 
82
+ Drop `Trust Server Certificate=true` from older samples (including Neon's own): since Npgsql 6 it is obsolete, and on Npgsql 10 the builder's attribute reads "no longer needed and does nothing". `SSL Mode=Require` encrypts; `SSL Mode=VerifyFull` also validates the certificate chain and host name.
83
+
73
84
  ### DbContext Registration
74
85
 
75
86
  ```csharp
76
- builder.Services.AddDbContext<AppDbContext>(options =>
87
+ builder.Services.AddDbContext<ApplicationDbContext>(options =>
77
88
  options.UseNpgsql(
78
89
  builder.Configuration.GetConnectionString("DefaultConnection"),
79
90
  npgsqlOptions =>
@@ -89,24 +100,30 @@ builder.Services.AddDbContext<AppDbContext>(options =>
89
100
  ### Design-Time Factory (for migrations using direct connection)
90
101
 
91
102
  ```csharp
92
- public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
103
+ using Microsoft.EntityFrameworkCore;
104
+ using Microsoft.EntityFrameworkCore.Design;
105
+
106
+ public sealed class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
93
107
  {
94
- public AppDbContext CreateDbContext(string[] args)
108
+ public ApplicationDbContext CreateDbContext(string[] args)
95
109
  {
96
110
  var configuration = new ConfigurationBuilder()
97
111
  .AddJsonFile("appsettings.json")
112
+ .AddUserSecrets<ApplicationDbContextFactory>(optional: true)
98
113
  .AddEnvironmentVariables()
99
114
  .Build();
100
115
 
101
- var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
116
+ var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
102
117
  optionsBuilder.UseNpgsql(
103
118
  configuration.GetConnectionString("MigrationConnection"));
104
119
 
105
- return new AppDbContext(optionsBuilder.Options);
120
+ return new ApplicationDbContext(optionsBuilder.Options);
106
121
  }
107
122
  }
108
123
  ```
109
124
 
125
+ `AddUserSecrets` reads the same secrets the app uses in development, and `AddEnvironmentVariables` (last, so it wins) lets CI pass `ConnectionStrings__MigrationConnection`.
126
+
110
127
  ## Branching Workflow
111
128
 
112
129
  Neon branches are copy-on-write clones of your database -- instant, zero-cost until data diverges.
@@ -149,8 +166,10 @@ Neon automatically suspends computes after inactivity (default: 5 minutes). This
149
166
  ### Mitigate Cold Starts
150
167
 
151
168
  ```csharp
169
+ var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
170
+
152
171
  // 1. Configure retry policy to handle connection drops on wake-up
153
- builder.Services.AddDbContext<AppDbContext>(options =>
172
+ builder.Services.AddDbContext<ApplicationDbContext>(options =>
154
173
  options.UseNpgsql(connectionString, npgsqlOptions =>
155
174
  {
156
175
  npgsqlOptions.EnableRetryOnFailure(
@@ -159,32 +178,35 @@ builder.Services.AddDbContext<AppDbContext>(options =>
159
178
  errorCodesToAdd: null);
160
179
  }));
161
180
 
162
- // 2. Health check to warm the connection pool
181
+ // 2. Health check to warm the connection pool (package AspNetCore.HealthChecks.NpgSql)
163
182
  builder.Services.AddHealthChecks()
164
- .AddNpgSql(connectionString, name: "neon-db");
183
+ .AddNpgSql(connectionString!, name: "neon-db");
165
184
  ```
166
185
 
167
186
  ### Suspend Timeout Configuration
168
187
 
169
- ```bash
170
- # Set suspend timeout to 10 minutes (600 seconds)
171
- neonctl branches create --project-id <project-id> \
172
- --name main \
173
- --suspend-timeout 600
188
+ Set it when the branch (and its compute) is created; `main` already exists, so it is never recreated to change a setting:
174
189
 
175
- # Compute size range (autoscaling)
190
+ ```bash
191
+ # New branch with a 10-minute suspend timeout and an autoscaling range
176
192
  neonctl branches create --project-id <project-id> \
177
- --name main \
193
+ --name feature/my-feature \
194
+ --parent main \
195
+ --suspend-timeout 600 \
178
196
  --cu "0.25-4"
179
197
  ```
180
198
 
199
+ For an **existing** compute (e.g. `main`'s), change `suspend_timeout_seconds` in the Console or with the API (`PATCH /projects/{project-id}/endpoints/{endpoint-id}` with `{"endpoint":{"suspend_timeout_seconds":600}}`; default 300, min 60, max 604800), with the Neon API key read from the environment, never typed into the command.
200
+
181
201
  ## Common Mistakes
182
202
 
183
203
  | Wrong | Right | Why |
184
204
  |-------|-------|-----|
185
205
  | Direct connection for app queries | Pooled (`-pooler`) connection | Direct connections exhaust compute limits |
186
206
  | No retry policy | `EnableRetryOnFailure()` | Cold starts drop first connection |
187
- | Hardcoded connection strings | Environment variables | Branches have unique endpoints |
207
+ | Hardcoded / committed connection strings | user-secrets (dev), env vars (Coolify) | Branches have unique endpoints, and a committed password leaks |
208
+ | `postgresql://` URI in `ConnectionStrings` | Keyword format (`Host=...;SSL Mode=Require`) | Npgsql does not parse URIs |
209
+ | `Trust Server Certificate=true` | Drop it (`SSL Mode=Require` or `VerifyFull`) | Obsolete; does nothing on Npgsql 10 |
188
210
  | Permanent CI branches | `--expires-at` TTL | Orphaned branches waste resources |
189
211
  | Running migrations on pooled connection | Direct connection for `dotnet ef` | PgBouncer doesn't support advisory locks |
190
212
  | No SSL | `SSL Mode=Require` | Neon requires encrypted connections |
@@ -183,10 +183,10 @@ internal sealed class Create{{pascalCase FEATURE_NAME}}Endpoint : IApiEndpoint
183
183
  var result = await handler.HandleAsync(command, cancellationToken);
184
184
  return result.Match(
185
185
  onSuccess: () => Results.Ok(result.Value),
186
- onFailure: error => Results.BadRequest(error));
186
+ onFailure: error => error.ToHttpResult());
187
187
  })
188
188
  .WithTags("{{pascalCase FEATURE_NAME}}s")
189
189
  .Produces<Create{{pascalCase FEATURE_NAME}}Response>(StatusCodes.Status200OK)
190
- .Produces(StatusCodes.Status400BadRequest);
190
+ .ProducesProblem(StatusCodes.Status400BadRequest);
191
191
  }
192
192
  }
@@ -11,7 +11,7 @@ namespace {{pascalCase PROJECT_NAME}}.Extensions
11
11
 
12
12
  services.AddDbContext<ApplicationDbContext>(options =>
13
13
  {
14
- options.UseNpgsql(configuration.GetConnectionString("connection"));
14
+ options.UseNpgsql(configuration.GetConnectionString("DefaultConnection"));
15
15
  });
16
16
 
17
17
  return services;
@@ -25,3 +25,30 @@ public static class ResultExtensions
25
25
  : onFailure(result.Error);
26
26
  }
27
27
  }
28
+
29
+ public static class ErrorHttpMapping
30
+ {
31
+ public static int StatusCodeFor(ErrorType type) => type switch
32
+ {
33
+ ErrorType.Validation => StatusCodes.Status400BadRequest,
34
+ ErrorType.NotFound => StatusCodes.Status404NotFound,
35
+ ErrorType.Conflict => StatusCodes.Status409Conflict,
36
+ ErrorType.Unauthorized => StatusCodes.Status401Unauthorized,
37
+ ErrorType.Forbidden => StatusCodes.Status403Forbidden,
38
+ ErrorType.Unexpected => StatusCodes.Status500InternalServerError,
39
+ _ => StatusCodes.Status400BadRequest
40
+ };
41
+
42
+ public static IResult ToHttpResult(this Error error)
43
+ {
44
+ Dictionary<string, object?>? extensions = error is ValidationError validation
45
+ ? new() { ["errors"] = validation.Errors.Select(e => new { e.Code, e.Description }).ToArray() }
46
+ : null;
47
+
48
+ return Results.Problem(
49
+ title: error.Code,
50
+ detail: error.Description,
51
+ statusCode: StatusCodeFor(error.Type),
52
+ extensions: extensions);
53
+ }
54
+ }
@@ -12,10 +12,10 @@ internal sealed class {{pascalCase SLICE_NAME}}Endpoint : IApiEndpoint
12
12
  var result = await handler.HandleAsync(command, cancellationToken);
13
13
  return result.Match(
14
14
  onSuccess: () => Results.Ok(result.Value),
15
- onFailure: error => Results.BadRequest(error));
15
+ onFailure: error => error.ToHttpResult());
16
16
  })
17
17
  .WithTags(ApiTags.{{pascalCase FEATURE_NAME}}s)
18
18
  .Produces<{{pascalCase SLICE_NAME}}Response>(StatusCodes.Status200OK)
19
- .Produces(StatusCodes.Status400BadRequest);
19
+ .ProducesProblem(StatusCodes.Status400BadRequest);
20
20
  }
21
21
  }
@@ -49,7 +49,7 @@ public sealed class MediaDisclosureTests
49
49
  Assert.False(
50
50
  result.IsSuccess,
51
51
  "publicar sem marcacao e o caminho que a obrigacao legal existe para fechar; " +
52
- "se ele existe, a afirmacao da rubrica media-claims esta presa so por prosa");
52
+ "se ele existe, a prova de midia do Gate 3 esta presa so por prosa");
53
53
  Assert.Equal(MediaErrorCode.InvalidTransition, result.Error!.Code);
54
54
  Assert.Contains("marcacao", result.Error.Message, StringComparison.OrdinalIgnoreCase);
55
55
  }
@@ -3,10 +3,10 @@
3
3
  "provider": "coolify",
4
4
  "access": {
5
5
  "method": "ssh",
6
- "note": "No MCP/CLI exists for Coolify — replace sshHost with the real SSH alias for the target VPS. The API is reached only from inside the VPS, at apiBaseUrl.",
6
+ "note": "We use no Coolify MCP/CLI — replace sshHost with the real SSH alias for the target VPS and tokenPath with the chmod-600 token file in that user's home. The API is reached only from inside the VPS, at apiBaseUrl; the deploy trigger is POST /deploy?uuid= (GET is refused by Coolify 4.3.23).",
7
7
  "sshHost": "REPLACE_WITH_VPS_SSH_ALIAS",
8
8
  "apiBaseUrl": "http://localhost:8000/api/v1",
9
- "tokenPath": "~/.coolify-token"
9
+ "tokenPath": "~/REPLACE_WITH_TOKEN_FILE"
10
10
  },
11
11
  "application": {
12
12
  "uuid": "REPLACE_WITH_COOLIFY_APPLICATION_UUID",
@@ -45,5 +45,5 @@
45
45
  "https": true
46
46
  }
47
47
  ],
48
- "deployCommand": "ssh REPLACE_WITH_VPS_SSH_ALIAS 'T=$(cat ~/.coolify-token); curl -s -X GET -H \"Authorization: Bearer $T\" \"http://localhost:8000/api/v1/deploy?uuid=REPLACE_WITH_COOLIFY_APPLICATION_UUID\"'"
48
+ "deployCommand": "ssh REPLACE_WITH_VPS_SSH_ALIAS 'T=$(cat ~/REPLACE_WITH_TOKEN_FILE); curl -s -X POST -H \"Authorization: Bearer $T\" \"http://localhost:8000/api/v1/deploy?uuid=REPLACE_WITH_COOLIFY_APPLICATION_UUID\"'"
49
49
  }