@polymorphism-tech/morph-spec 10.0.0 → 10.2.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.
- package/CLAUDE.md +2 -0
- package/README.md +3 -7
- package/bin/morph-spec.js +1 -41
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +27 -38
- package/docs/QUICKSTART.md +31 -191
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +2 -2
- package/framework/agents.json +0 -1
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-preflight.md +6 -7
- package/framework/docs/CLI.md +24 -21
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/core/validate-completion.js +0 -35
- package/framework/hooks/shared/e2e-check.js +44 -7
- package/framework/hooks/shared/heavy-command.js +12 -1
- package/framework/hooks/shared/next-step.js +4 -4
- package/framework/mcp/registry.json +4 -4
- package/framework/phases.json +4 -4
- package/framework/rules/ai-agents-standards.md +1 -1
- package/framework/rules/data-standards.md +1 -1
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-auth-standards.md +1 -1
- package/framework/rules/neon-standards.md +3 -3
- package/framework/schemas/config.schema.json +20 -1
- package/framework/schemas/feature.schema.json +1 -0
- 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 +8 -7
- package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
- package/framework/skills/core/morph-apply/references/review.md +5 -5
- package/framework/skills/core/morph-plan/SKILL.md +22 -16
- package/framework/skills/core/morph-plan/references/critique.md +11 -8
- package/framework/skills/core/morph-plan/references/interview.md +18 -6
- package/framework/skills/core/morph-uiux/SKILL.md +2 -2
- package/framework/standards/STANDARDS.json +20 -24
- package/framework/standards/STANDARDS.meta.json +12 -19
- package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -0
- package/framework/standards/architecture/stack-baseline.md +26 -3
- package/framework/standards/backend/database/postgresql/database.md +22 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +10 -9
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +2 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
- package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
- package/framework/standards/integration/mcp/mcp-tools.md +20 -20
- package/framework/templates/docs/plan.md +4 -1
- package/framework/templates/examples/design-system-examples.md +0 -3
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
- package/package.json +1 -1
- package/src/commands/project/doctor.js +0 -1
- package/src/commands/state/approve.js +9 -109
- package/src/commands/state/auto-approval.js +1 -1
- package/src/commands/state/create.js +1 -1
- package/src/commands/state/gate-check.js +2 -2
- package/src/commands/state/report-check.js +44 -2
- package/src/commands/validation/verify.js +7 -3
- package/src/core/state/state-manager.js +0 -24
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/heavy/cleanup.js +8 -2
- package/src/lib/installers/memory-installer.js +1 -1
- package/src/lib/plan/checks.js +8 -1
- package/src/lib/plan/critique.js +23 -3
- package/src/lib/review-confirmation.js +67 -0
- package/src/lib/review-reports.js +1 -0
- package/src/lib/risk-detector.js +2 -2
- package/src/lib/tasks/test-nodes.js +5 -0
- package/src/lib/tasks/test-runner.js +37 -43
- package/src/lib/telemetry/telemetry-reader.js +1 -2
- package/src/lib/telemetry/verify-event.js +7 -0
- package/src/lib/validators/verify-cache.js +63 -0
- package/src/lib/validators/verify-runner.js +98 -23
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/hooks-installer.js +0 -20
- package/bin/validate.js +0 -212
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
- package/src/commands/observability/telemetry.js +0 -60
- package/src/commands/validation/validate-feature.js +0 -337
|
@@ -26,15 +26,14 @@ These checks verify that the morph-spec workflow is complete for the feature bei
|
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
# Verify implementation is complete
|
|
29
|
-
morph-spec status {feature}
|
|
30
|
-
morph-spec approval-status {feature}
|
|
29
|
+
morph-spec status {feature} --verbose
|
|
31
30
|
morph-spec verify {feature}
|
|
32
31
|
```
|
|
33
32
|
|
|
34
33
|
| Check | Command | Pass Criteria |
|
|
35
34
|
|-------|---------|---------------|
|
|
36
35
|
| Phase is implement or later | `status` | phase derived as `implement` or `review` |
|
|
37
|
-
| All gates approved | `
|
|
36
|
+
| All gates approved | `status --verbose` | plan (and uiux, when the feature has design) and review gates approved |
|
|
38
37
|
| Build, tests and validators pass | `verify` | every node green |
|
|
39
38
|
| All slices reported | `status` → Fatias | 0 slices pending or partial |
|
|
40
39
|
|
|
@@ -42,7 +41,7 @@ morph-spec verify {feature}
|
|
|
42
41
|
|
|
43
42
|
---
|
|
44
43
|
|
|
45
|
-
**Tests:**
|
|
44
|
+
**Tests:** the proof the gates read is `morph-spec verify {feature}` without `--filter`. A bare `dotnet test`/`npm test` is fine for checking something (it goes through the heavy-job queue), but it leaves no proof. Any red node blocks deployment.
|
|
46
45
|
|
|
47
46
|
---
|
|
48
47
|
|
|
@@ -72,15 +71,15 @@ dotnet restore --verbosity normal 2>&1 | grep -E "(NU1605|NU1608|warning)"
|
|
|
72
71
|
**How to check:**
|
|
73
72
|
```bash
|
|
74
73
|
# Step 0: Verify database connectivity
|
|
75
|
-
psql "$(
|
|
74
|
+
psql "$(neon connection-string)" -c "SELECT 1" 2>/dev/null && \
|
|
76
75
|
echo "✓ Database reachable" || \
|
|
77
76
|
echo "✗ Database unreachable — skip remaining migration checks"
|
|
78
77
|
|
|
79
78
|
# Check for pending migrations (skip if connectivity failed)
|
|
80
|
-
|
|
79
|
+
neon branches schema-diff <base-branch> <branch>
|
|
81
80
|
|
|
82
81
|
# Verify RLS is enabled on all tables
|
|
83
|
-
psql "$(
|
|
82
|
+
psql "$(neon connection-string)" -c "SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename NOT IN (SELECT tablename FROM pg_policies)"
|
|
84
83
|
```
|
|
85
84
|
|
|
86
85
|
> **Important:** If the database connectivity check fails, skip all migration checks and warn the user. Do not block the preflight — the database may be intentionally scaled to zero.
|
package/framework/docs/CLI.md
CHANGED
|
@@ -64,7 +64,7 @@ A tabela é a mesma régua que `shouldPauseGate` aplica e que o `gate-check` imp
|
|
|
64
64
|
| `screenChecklist` | item em aberto | **uiux**: tela sem checklist completo, nenhuma `## Tela:`, ou tela sem protótipo, prints 1280/390, ou seletor e print de cada estado. **ship**: item decidido sem linha em `## Checklist de tela` |
|
|
65
65
|
| `planChecks` | ≠ `fresh` | `checks.json` ausente ou de outra versão do `plan.md`: rode `plan-check`. Só no **plano**; `approve`/`advance` automáticos recusam |
|
|
66
66
|
| `workType` | === 'hotfix' | Gate 3 pausa SEMPRE (ver "Gates por tipo de trabalho") |
|
|
67
|
-
| `planCritique` | `missing`/`no-hash`/`stale` | `2-plan/critica.md` ausente, sem `**Plan-Hash:**`, ou de outra versão do plano sem `**Aplicado-em:**` que cubra a atual (`signal: plan-critique`). Só no **plano**, `chore`/`hotfix` isentos (`exempt`); `approve`/`advance` automáticos recusam |
|
|
67
|
+
| `planCritique` | `missing`/`no-hash`/`stale` | `2-plan/critica.md` ausente, sem `**Plan-Hash:**`, ou de outra versão do plano sem `**Aplicado-em:**` que cubra a atual (`signal: plan-critique`). Só no **plano**, `chore`/`hotfix` e plano pequeno isentos (`exempt`: até 3 cenários, todos `prova: teste` ou `leitura`, sem degrau e sem mídia gerada); `approve`/`advance` automáticos recusam |
|
|
68
68
|
| `rungs` | `Cn` com `degrau: migrar` ou `irreversível` (`checks.json`) | **plano**: pausa nomeando os `Cn` (`signal: rung`), e `approve`/`advance` automáticos recusam. **ship**: pausa se o plano não teve aprovação humana (chore, policy) |
|
|
69
69
|
| `migrationsTouched` | caminho `migrations/` no diff, `rungs` vazio | Feature mexeu em dado sem declarar o degrau (`signal: rung-undeclared`). Só no **ship** |
|
|
70
70
|
| `contractsTouched` | `openapi.{yaml,yml,json}`, `swagger.{…}` ou `.proto` no diff, sem `Cn` `irreversível` | O arquivo de contrato mudou sem o degrau (`signal: rung-undeclared`). Webhook e endpoint ficam com o bloqueador `irreversivel` do revisor. Só no **ship** |
|
|
@@ -89,7 +89,7 @@ A tabela é a mesma régua que `shouldPauseGate` aplica e que o `gate-check` imp
|
|
|
89
89
|
|
|
90
90
|
### create
|
|
91
91
|
|
|
92
|
-
**`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request
|
|
92
|
+
**`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request '<txt>'] [--name "<nome>"] [--worktree] [--no-checkout]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início do plano, `morph-plan` §1; o hook state-sync continua cobrindo o caminho implícito como fallback).
|
|
93
93
|
|
|
94
94
|
**Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio.
|
|
95
95
|
|
|
@@ -125,7 +125,7 @@ Feature sem `workType` (pré-v8) é tratada como `feature`.
|
|
|
125
125
|
|
|
126
126
|
### finish
|
|
127
127
|
|
|
128
|
-
**`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo).
|
|
128
|
+
**`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum; a sessão escolhe por `finish.mode` do `config.json`, padrão `pr`): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo).
|
|
129
129
|
|
|
130
130
|
**Índice de ADRs:** no mesmo commit do archive, o `finish` lê o `2-plan/plan.md` da feature: os ADRs de `## Decisões` com `**Alcance:** projeto` entram em `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo), e a tabela `ADR | Mudança | Escopo` de `### Conflitos com o acervo` retira (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os ADRs revogados — forma em `morph-spec plan-check --help`; o `plan-check` avisa, enquanto o plano ainda muda, a linha que o `finish` não aplicaria. O payload traz `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`.
|
|
131
131
|
|
|
@@ -277,7 +277,11 @@ Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o r
|
|
|
277
277
|
|
|
278
278
|
### verify
|
|
279
279
|
|
|
280
|
-
**`morph-spec verify <feature> [--json] [--timeout <ms>] [--filter <ids>] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist] [--ci]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM), sempre sobre a feature inteira. Antes da cadeia, instala em `.claude/rules/` (fora do git) a regra de stack que falta, pela stack do `config.json` somada à que os arquivos mostram (em stderr; nunca muda o veredito). Nunca grava o `config.json`: arquivo versionado mudado pelo próprio verify tornaria a prova velha e a árvore suja para o `--ci`; a tag nova entra pelo `plan-check` ou pelo `update`.
|
|
280
|
+
**`morph-spec verify <feature> [--json] [--timeout <ms>] [--filter <ids>] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--fresh] [--no-persist] [--ci]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM), sempre sobre a feature inteira. Antes da cadeia, instala em `.claude/rules/` (fora do git) a regra de stack que falta, pela stack do `config.json` somada à que os arquivos mostram (em stderr; nunca muda o veredito). Nunca grava o `config.json`: arquivo versionado mudado pelo próprio verify tornaria a prova velha e a árvore suja para o `--ci`; a tag nova entra pelo `plan-check` ou pelo `update`.
|
|
281
|
+
|
|
282
|
+
**Reaproveitamento entre rodadas:** uma rodada completa grava o resultado por nó em `.git/morph-spec/verify-nodes-<feature>.json`, chaveado pela impressão da árvore (a régua de `treeFingerprint`). Mesma árvore: build, suíte e e2e verdes não rodam de novo (`reused[]`; os validadores sempre rodam), e o vermelho que passa agora entra em `flaky[]`, que o stamp guarda em `verification.flaky` para o verificador. Árvore mudada: nada é aproveitado; as suítes vermelhas da rodada anterior rodam primeiro e, se uma seguir vermelha, as outras ficam `skip` (`adiado: …`) e a rodada sai vermelha. O stamp verde sempre exige todos os nós verdes na árvore atual. Rodada parcial (`--filter`, `--skip-*`, `--linux`, `--record-baseline`, `--refresh-cache`, `--fresh-e2e`) não lê nem grava. O que a impressão não vê (`.env`, banco, versão de ferramenta): `--fresh` roda tudo.
|
|
283
|
+
|
|
284
|
+
**Reexecução de spec que falhou:** com Playwright (`npx playwright test`), specs vermelhas rodam mais uma vez com `--last-failed` (exige Playwright 1.44+). Passaram: nó verde, com os nomes em `flaky[]`. Falharam: nó vermelho. `e2e.command` próprio sem `playwright` como argumento não reexecuta. Cada rodada de specs tem teto de `e2e.specTimeoutMs` (padrão 300 000 ms).
|
|
281
285
|
|
|
282
286
|
O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável, sobe os `e2e.hostServices` (ver `e2e` abaixo) e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Sem compose mas com `e2e.hostServices`, a stack é **só de host**: sobem só os serviços do host, sem lint de isolamento do compose nem checagem de Docker, e as specs rodam contra eles. Sem compose e sem `e2e.hostServices`, o nó pula com `project does not containerize (no compose file)`; com a integração `docker` declarada e sem compose, falha.
|
|
283
287
|
|
|
@@ -311,7 +315,7 @@ Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de con
|
|
|
311
315
|
|
|
312
316
|
**Validador que não pôde checar:** o `reuse` sem grafo não acha nada, e um `validators: pass` dos outros validadores o esconderia — então o `feedback[]` diz `[validators/reuse] não rodou: …`, e o nó leva `skipped: [{ validator, reason, unavailable }]` (quando o nó inteiro pula, a própria linha dele já dá o motivo).
|
|
313
317
|
|
|
314
|
-
**Nós pesados (build, testes, e2e) esperam por slot** (`morph-spec heavy`): estouro da espera sai **exit 3** com `status: "busy"`, `persisted: false`, sem stamp e sem evento — não é vermelho, e
|
|
318
|
+
**Nós pesados (build, testes, e2e) esperam por slot** (`morph-spec heavy`): estouro da espera sai **exit 3** com `status: "busy"`, `persisted: false`, sem stamp e sem evento — não é vermelho: `morph-spec heavy status` mostra quem segura a vaga, e cabe no máximo uma chamada nova.
|
|
315
319
|
|
|
316
320
|
**Por quê.** Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar.
|
|
317
321
|
|
|
@@ -325,7 +329,7 @@ Nenhuma regra de gate muda: e2e pulado, escopo, evals e stale valem igual.
|
|
|
325
329
|
|
|
326
330
|
**Sem CI cai para o verify local, com a razão** (`project.ci: false`, origin fora do GitHub, `gh` ausente ou sem login, branch que não é `morph/<feature>`, token HTTPS sem o escopo `workflow` para criar o arquivo, push recusado, workflow que não disparou nem com `gh workflow run`, run sem o artefato). **Antes de tudo — com ou sem CI — reaproveita o stamp fresco:** último stamp `pass` com `sha`, nada fora de commit e `treeMovedSince(stamp.sha, HEAD)` falso (a mesma régua do `verificationStale`: só `.morph/` andou, ou o `config.json` mudou só `expectedCount`) → não empurra, não roda nem regrava, imprime "stamp verde e fresco — o CI não roda de novo" e o `--json` traz `reason: "stamp-reused"`, `status`, `nodes` e `reused: { sha, ts, source }`, exit 0.
|
|
327
331
|
|
|
328
|
-
Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rodando depois de 50 min é **exit 3** (`status: "pending"`), sem stamp — não é vermelho. Com
|
|
332
|
+
Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rodando depois de 50 min é **exit 3** (`status: "pending"`), sem stamp — não é vermelho. Com `--filter`, `--skip-*`, `--fresh-e2e`, `--linux`, `--record-baseline`, `--refresh-cache` ou `--no-persist`, erro de uso. Vermelho imprime `gh run view <id> --log-failed`.
|
|
329
333
|
|
|
330
334
|
**O runner só tem o que está commitado:** o que testes e e2e leem de arquivo gitignored (`.env`, `appsettings.*.json` local, connection string do Neon, `storageState`) não existe lá, e o run sai vermelho sem defeito de código. Declare o valor como secret do repositório e exponha-o no `env:` do job em `morph-verify.yml` (o arquivo é do projeto: editar à mão é o esperado, e o `findCiWorkflow` continua aceitando enquanto ele chamar `morph-spec verify` publicando `morph-verify.json`), ou desligue com `project.ci: false`.
|
|
331
335
|
|
|
@@ -341,7 +345,7 @@ Teste é o que o validador `reuse` já trata como teste: arquivo `.test.` ou `.s
|
|
|
341
345
|
|
|
342
346
|
Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falha de medição também vira skip, nunca erro.
|
|
343
347
|
|
|
344
|
-
**Por quê.** Excesso justificado não silencia o aviso: a justificativa mora
|
|
348
|
+
**Por quê.** Excesso justificado não silencia o aviso: a justificativa mora no plano, junto do `Cn`, e é lá que o revisor a lê.
|
|
345
349
|
|
|
346
350
|
### e2e
|
|
347
351
|
|
|
@@ -408,11 +412,11 @@ Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os con
|
|
|
408
412
|
|
|
409
413
|
Env: `MORPH_HEAVY=off` desliga o semáforo (a suíte do próprio framework roda assim, para não enfileirar nos slots reais de quem a executa), `MORPH_HEAVY_SLOTS`, `MORPH_HEAVY_DIR`.
|
|
410
414
|
|
|
411
|
-
**Fim do trabalho pesado:** no fim do `verify`
|
|
415
|
+
**Fim do trabalho pesado:** no `e2e down`, no `doctor --procs --prune` e no fim do `verify` quando a RAM livre está abaixo do dobro de `heavy.minFreeMemGb` (com folga, o compilador fica quente para a próxima rodada), roda `dotnet build-server shutdown` **só** se o projeto é .NET, `heavy.shutdownBuildServer` está ligado, nenhum slot está preso **e** nenhum `dotnet build|test|publish|msbuild` está vivo — sub-agents e IDE não passam pelo semáforo, então semáforo vazio não basta. Fail-open, timeout de 30 s, resultado em `cleanup` no `--json`.
|
|
412
416
|
|
|
413
417
|
`heavy.disableNodeReuse` põe `MSBUILDDISABLENODEREUSE=1` enquanto o nó pesado roda e restaura depois. O `--linux` rotula o container (`morph-spec.verify=<runId>`, `morph-spec.root=<hash da raiz>`) e o remove **por label** no fim, nunca por nome; o `e2e down` usa `--remove-orphans`. O `verify` **não** derruba a stack e2e (`keepUp` é por desenho).
|
|
414
418
|
|
|
415
|
-
**Bash cru (`heavy.gateBash`, padrão `true`):** a sessão roda build e teste quando quer, e um `dotnet build`/`dotnet test`/`npm test` pelo Bash não passa pelo `verify`, então dois elos das chains de hooks fazem o papel dele. `heavy-gate` (`PreToolUse` Bash, depois de todos os guards e antes do `worktree-env-inject`) reconhece o comando pesado — `dotnet build|test|publish|pack|msbuild|vstest`, `msbuild`, `npm|pnpm|yarn|bun test|build` e `run test|build[:x]`, `npx vitest|jest|playwright test|next build`, `vitest`, `jest`, `playwright test`, `next|vite build`, `docker build|buildx`, `docker compose|docker-compose up|build` — olhando o executável de cada segmento (`;`, `&&`, `||`, `|`, quebra de linha, fora de aspas): `git commit -m "fix build && dotnet test"` e `morph-spec heavy run -- dotnet test` não são pesados. Toma um slot e espera até `min(waitTimeoutSec, 540)` s — abaixo dos 600 s de timeout do hook, porque um `PreToolUse` que estoura o timeout **deixa a chamada rodar**; sem vaga no prazo a chamada é **recusada** (`deny`) com a causa e a instrução de repetir o mesmo comando, e não roda.
|
|
419
|
+
**Bash cru (`heavy.gateBash`, padrão `true`):** a sessão roda build e teste quando quer, e um `dotnet build`/`dotnet test`/`npm test` pelo Bash não passa pelo `verify`, então dois elos das chains de hooks fazem o papel dele. `heavy-gate` (`PreToolUse` Bash, depois de todos os guards e antes do `worktree-env-inject`) reconhece o comando pesado — `dotnet build|test|publish|pack|msbuild|vstest`, `msbuild`, `npm|pnpm|yarn|bun test|build` e `run test|build[:x]`, `npx vitest|jest|playwright test|next build`, `vitest`, `jest`, `playwright test`, `next|vite build`, `docker build|buildx`, `docker compose|docker-compose up|build`, `powershell|pwsh` com um `.ps1` cujo nome contém `test` — olhando o executável de cada segmento (`;`, `&&`, `||`, `|`, quebra de linha, fora de aspas): `git commit -m "fix build && dotnet test"` e `morph-spec heavy run -- dotnet test` não são pesados. Toma um slot e espera até `min(waitTimeoutSec, 540)` s — abaixo dos 600 s de timeout do hook, porque um `PreToolUse` que estoura o timeout **deixa a chamada rodar**; sem vaga no prazo a chamada é **recusada** (`deny`) com a causa e a instrução de repetir o mesmo comando, e não roda.
|
|
416
420
|
|
|
417
421
|
`heavy-release` (`PostToolUse` e `PostToolUseFailure`) devolve a vaga pelo `tool_use_id`.
|
|
418
422
|
|
|
@@ -496,7 +500,9 @@ A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhu
|
|
|
496
500
|
|
|
497
501
|
**Bloco `tree`**, em toda saída e sem mudar o exit code: `dirty`/`dirtyCount` (o que não está commitado fora de `.morph/`), `diffstat` (contra `HEAD`), `commits` (sobre a default, até 10; `null` sem default), `fingerprint` (a mesma impressão da árvore que o `verify` grava), `head` e `branch`; `null` quando o git não responde — desconhecido, nunca árvore limpa.
|
|
498
502
|
|
|
499
|
-
**`--before-eval`** grava em `4-review/tree-before-eval.json` a foto da árvore (HEAD e o blob de cada arquivo que difere dele, fora de `.morph/`) antes do dispatch da review, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`; sem foto, `null`. Traz também `reviewModels`: `verifier.model` (`sonnet` por padrão, `opus` com prompt/modelo tocado ou plano que cita standard de mídia, com `reasons`), `reviewer.model` (`sonnet`), `security: { required, model: 'opus', reasons }` (exigida quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration) e `required`, a lista de papéis que o Gate 3 cobra — a mesma regra do `gate-check` e do `approve`.
|
|
503
|
+
**`--before-eval`** grava em `4-review/tree-before-eval.json` a foto da árvore (HEAD e o blob de cada arquivo que difere dele, fora de `.morph/`) antes do dispatch da review, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`; sem foto, `null`. Traz também `reviewModels`: `verifier.model` (`sonnet` por padrão, `opus` com prompt/modelo tocado ou plano que cita standard de mídia, com `reasons`), `reviewer.model` (`sonnet`), `security: { required, model: 'opus', reasons }` (exigida quando o diff toca auth, segredo, permissão, dado pessoal, pagamento ou migration) e `required`, a lista de papéis que o Gate 3 cobra — a mesma regra do `gate-check` e do `approve`. Traz ainda `gate3: { base, sha, flaky, e2eChecks, redispatch }`: `base` é o merge-base com a default, `flaky` vem do stamp do verify, `e2eChecks` lista os `Cn` de `prova: e2e`, e `redispatch` diz quem volta depois de uma correção (`null` sem relatório: todos os exigidos vão).
|
|
504
|
+
|
|
505
|
+
**Quem volta depois de uma correção** (`reviewConfirmation`, a mesma regra no `approve`): papel com relatório atrás do `HEAD` volta, salvo SHIP limpo com delta estreito: arquivos citados nos bloqueadores (caminho ou nome) ou de teste; para a segurança, nenhum caminho sensível. O `approve` só dispensa esse papel com um papel que abriu bloqueador revisando o `HEAD`.
|
|
500
506
|
|
|
501
507
|
### plan-check
|
|
502
508
|
|
|
@@ -545,6 +551,7 @@ em runtime é `validateTestNodes()` — schema que ninguém chama é decoração
|
|
|
545
551
|
| `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify |
|
|
546
552
|
| `cwd` | não (`"."`) | Relativo à raiz. Path que resolve **fora** da raiz é recusado (endereça outra árvore); `cwd` inexistente é `fail`, não `skip` — um skip esconderia erro de plano atrás de gate verde |
|
|
547
553
|
| `runner` | não (inferido) | `dotnet` \| `vitest` \| `jest` \| `node-test` \| `playwright` \| `none` \| `evals`. Escolhe o extrator de contagem e se há prebuild. `none` = contagem DESCONHECIDA, nunca zero. `evals` é o único que **nunca é inferido** e o único que exige um bloco próprio — ver a subseção abaixo |
|
|
554
|
+
| `parallel` | não | `true` roda o nó junto dos outros nós com `parallel: true`. Declare só em suítes que não dividem banco, porta nem pasta de build: o harness não tem como conferir. Ignorado em nó `e2e: true` e numa rodada que persegue suíte vermelha |
|
|
548
555
|
| `expectedCount` | não | Baseline, só nos runners de contagem estável (`node-test`, `evals`); nos outros é ignorada e o `--record-baseline` a apaga. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
|
|
549
556
|
| `timeoutMs` | não | Teto deste nó. Sem ele vale `project.testTimeoutMs` e, por fim, o default de 30 min × projetos encadeados |
|
|
550
557
|
| `build` | não | Projetos a compilar antes (só `runner: "dotnet"`). Declarado em nó não-dotnet é erro de config, não campo silenciosamente ignorado |
|
|
@@ -691,8 +698,8 @@ testes reprova com `suíte verde sem nenhum teste executado` (suíte que parou d
|
|
|
691
698
|
**Prebuild e `--no-build` (por que um `dotnet test` verde podia mentir).** Num nó `runner: "dotnet"`,
|
|
692
699
|
os alvos de `build[]` — ou, na falta, os `.csproj` extraídos do próprio `command` — são compilados
|
|
693
700
|
antes da execução, com `dotnet build "<alvo>" --nologo -v q`. Compilação que falha reprova o nó com
|
|
694
|
-
`test-build-failed` e a suíte **não roda**. E um `command` com `--no-build` que **não** foi precedido de
|
|
695
|
-
build nesta invocação reprova com `stale-binary`, também sem executar: o predicado é "houve build
|
|
701
|
+
`test-build-failed` e a suíte **não roda**. E um `command` `dotnet` com `--no-build` que **não** foi precedido de
|
|
702
|
+
build nesta invocação (prebuild do nó ou `dotnet build` verde do nó `build`) reprova com `stale-binary`, também sem executar: o predicado é "houve build
|
|
696
703
|
aqui", nunca "a flag está ausente". Sem isso, um projeto de teste que deixou de compilar era executado
|
|
697
704
|
a partir do binário do último build bom e reportava verde. `--no-build` continua legítimo — com
|
|
698
705
|
`build[]` declarado ele roda normalmente, e o comando do usuário nunca é reescrito para isso. O prebuild é
|
|
@@ -721,10 +728,10 @@ nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha q
|
|
|
721
728
|
| CI | a suíte inteira, todos os nós (`morph-verify.yml` roda o mesmo `verify {f}`) | idem — e o artefato dele é o stamp do Gate 3 |
|
|
722
729
|
| Evidência de eval (cenário `Cn` com `prova: eval`) | `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist` | Só o nó `evals`, sem stack e sem carimbo — é a evidência de runtime do cenário, não a verificação da feature |
|
|
723
730
|
|
|
724
|
-
> **
|
|
725
|
-
>
|
|
726
|
-
> relógios que o
|
|
727
|
-
> encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms
|
|
731
|
+
> **Nenhum relógio do `verify` é de 600 s.** Os 600 s que existem são a espera da fila
|
|
732
|
+
> (`heavy.waitTimeoutSec`) e o teto do **Bash do harness**, externo ao morph-spec: é ele que corta uma
|
|
733
|
+
> suíte longa, e não há o que "subir". Os relógios que o `verify` de fato tem: build **1 800 000 ms** (30 min), tests **1 800 000 ms por projeto de teste
|
|
734
|
+
> encadeado**, `up --build` e health do e2e **180 000 ms**, specs Playwright **300 000 ms** (`e2e.specTimeoutMs`), mais `timeoutMs` por nó,
|
|
728
735
|
> que substitui o default do `tests` quando ele não serve. `--timeout <ms>` cobre build **e** tests.
|
|
729
736
|
> **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** rode
|
|
730
737
|
> `verify {feature}` com `run_in_background` e
|
|
@@ -828,10 +835,6 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
|
|
|
828
835
|
|
|
829
836
|
## Observabilidade
|
|
830
837
|
|
|
831
|
-
### telemetry
|
|
832
|
-
|
|
833
|
-
**`morph-spec telemetry [--json] [--feature <f>] [--limit <n>]`** — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, sub-agents despachados + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log`. Lê também os arquivos rotacionados em `.morph/logs/sessions/`, sem contar duas vezes a mesma linha.
|
|
834
|
-
|
|
835
838
|
### cost
|
|
836
839
|
|
|
837
840
|
**`morph-spec cost [feature] [--by phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: bytes de contexto lidos por tipo (`checklist`/`standard`/`skill`/`agents`/`plan-artifact`/`code` — a janela `offset`/`limit` que o Read trouxe, quando o evento a tem), dispatches + tamanho de prompt e mix de tools. Bytes são **proxy determinística** de custo de token; os tokens reais vêm à parte: `SessionUsage` (sessão principal) e `AgentUsage` (cada sub-agent, lido do transcript dele), somados **só pelo `delta`** de cada amostra — o total de topo é acumulado e se repete a cada Stop, e amostra antiga sem `delta` fica fora da soma e é contada como legada. `--all-trees` agrega **toda árvore git** com log, mais os logs que o `finish` guardou em `.git/morph-spec/finish-backup/` antes de desmontar a worktree, deduplicados por `eventId`.
|
|
@@ -866,6 +869,6 @@ Os eventos v1 (sem `schemaVersion`) nunca contam como zero: sem agente, exit cod
|
|
|
866
869
|
| `ToolBatch` | `size`, `tools` (contagem por ferramenta) — um por turno do modelo |
|
|
867
870
|
| `SessionUsage` / `AgentUsage` | totais acumulados do transcript + `delta` desde a amostra anterior |
|
|
868
871
|
| `Gate` | `gate`, `step` (`decision`/`approve`/`unapprove`); na `decision`: `pause`, `signal` (a regra que pausou, `null` sem pausa, `manual` pelo `gate-decision`) e `source`; nas outras: `approvedBy`. Gravado por `persistGateDecision` e pelos dois escritores de aprovação (CLI e hook `state-sync`) |
|
|
869
|
-
| `Verify` | `status`, `sha`, `fingerprint` (a árvore fora de `.morph/`), `tree` (`root`/`worktree`), `persisted`, `reason`, `source` (`local`/`ci`: a rodada do `--ci` registra os nós e durações do run), `nodes.{build,tests,validators,e2e}.{status,durationMs}`, `nodes.validators.{ran,skipped}` |
|
|
872
|
+
| `Verify` | `status`, `sha`, `fingerprint` (a árvore fora de `.morph/`), `tree` (`root`/`worktree`), `persisted`, `reason`, `source` (`local`/`ci`: a rodada do `--ci` registra os nós e durações do run), `nodes.{build,tests,validators,e2e}.{status,durationMs}`, `nodes.tests.runs[].{id,status,durationMs}` (qual suíte custou o quê), `reused[]`, `flaky[]`, `nodes.validators.{ran,skipped}` |
|
|
870
873
|
|
|
871
874
|
Nada da saída de um comando é gravado — nem o corpo de erro do `PostToolUseFailure`, onde um valor de `.env` pode aparecer sem nome ao lado. `command`, `prompt`, `desc` e `target` passam por redação de segredos óbvios antes de gravar: valor ao lado de um nome de segredo (`API_KEY=…`, `*_KEY`/`*_PASS=…`, `--password …`, `user-secrets set <k> <v>`, `AccountKey=…`), `Authorization: …`, `user:pass@` (senha com `/` inclusive) e `?sig=`/`?access_token=` em URL, e formatos de token (GitHub, `sk-`, `sk_live_`, `npm_`, Slack, AWS, Google, JWT). É por padrão, não por garantia: um valor sem nome ao lado passa.
|
|
@@ -33,7 +33,7 @@ typechecks or runs tests: that is `morph-spec verify`.
|
|
|
33
33
|
| **PostToolUse** (Write\|Edit) | comment-guard.js | Blocking | Blocks when the edit ADDED a comment the `architecture-code-comments` standard never allows: process history (ADR, scenario/slice id, gate, plan.md), banner/`#region`, a block over 3 lines, or a doc-comment outside a published package. Old comments are never reported; last link of the chain |
|
|
34
34
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state updates (feature register, output tracking, gate approval) |
|
|
35
35
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | gate-guard.js | Advisory | Warns (never blocks) when a gate action disagrees with the persisted gateDecisions |
|
|
36
|
-
| **PostToolUse** (all tools) | telemetry-log.js | Telemetry | Appends every tool call (skill, sub-agent dispatch + prompt, knowledge-base read, bash/edit) to `.morph/logs/events.jsonl`; silent, fail-open — the NLH execution-path capture read back by `morph-spec
|
|
36
|
+
| **PostToolUse** (all tools) | telemetry-log.js | Telemetry | Appends every tool call (skill, sub-agent dispatch + prompt, knowledge-base read, bash/edit) to `.morph/logs/events.jsonl`; silent, fail-open — the NLH execution-path capture read back by `morph-spec cost` |
|
|
37
37
|
| **PostToolUse** (all tools) | loop-detect.js | Advisory | Warns when the last 8 actions collapse to ≤2 unique signatures |
|
|
38
38
|
| **PostToolUse** (all tools) | graph-refresh-kick.js | Background | After a Bash or PowerShell command that runs git/gh/morph-spec, launches the DETACHED refresh of the ROOT's codebase graph (`shared/graph-refresh.js`: cwd in the git common dir, never a worktree; one lock per repository; staleness by `shared/graph-freshness.js` — stamp plus the per-tree run record in `<git-common-dir>/morph-spec/graph-refresh-state.json`); never waits, never injects |
|
|
39
39
|
| **PostToolUse** (all tools) | masked-exit.js | Advisory | After a Bash test/build/verify run (`testRunKind`/`morphVerifyCall`/`heavyKind` of the producer) whose exit `exitCodeMasked` hides (`\| tail`, `; echo`, `\|\| true`, `&`), names the form and says to write the output to `.morph/logs/test-last.log` and read it; silent when the command prints `$?`/`PIPESTATUS` |
|
|
@@ -41,7 +41,7 @@ typechecks or runs tests: that is `morph-spec verify`.
|
|
|
41
41
|
| **PostToolBatch** | telemetry-log.js | Telemetry | One `ToolBatch` event per model turn that used tools (the turn counter of `cost --metrics`) |
|
|
42
42
|
| **SubagentStop** | telemetry-log.js | Telemetry | `AgentUsage`: the sub-agent's token usage read from its own transcript, as a delta per agent; never blocks the stop |
|
|
43
43
|
| **UserPromptSubmit** | telemetry-log.js | Telemetry | Appends the user's prompt to the same `events.jsonl` stream; silent, fail-open |
|
|
44
|
-
| **Stop** | validate-completion.js | Advisory | Warns about
|
|
44
|
+
| **Stop** | validate-completion.js | Advisory | Warns about scenarios marked `feito` without evidence in `3-implement/report.md` (hash-deduped, only re-injects when the warning set changes) |
|
|
45
45
|
|
|
46
46
|
## Design Principles
|
|
47
47
|
|
|
@@ -20,10 +20,6 @@ function auditReport(featurePath) {
|
|
|
20
20
|
: [];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
24
|
-
|
|
25
|
-
const HEAVY_SOURCE_PHASES = new Set(['plan', 'uiux']);
|
|
26
|
-
|
|
27
23
|
export async function run(payload) {
|
|
28
24
|
try {
|
|
29
25
|
if (process.env.MORPH_STOP_HOOK_ACTIVE === '1') return null;
|
|
@@ -42,37 +38,6 @@ export async function run(payload) {
|
|
|
42
38
|
|
|
43
39
|
const currentPhase = feature.phase || derivePhaseForFeature(name, cwd);
|
|
44
40
|
|
|
45
|
-
const watchPath = join(memDir, 'phase-watch.json');
|
|
46
|
-
let phaseWatch = {};
|
|
47
|
-
try {
|
|
48
|
-
if (existsSync(watchPath)) phaseWatch = JSON.parse(readFileSync(watchPath, 'utf-8'));
|
|
49
|
-
} catch { /* corrupt file — start fresh */ }
|
|
50
|
-
|
|
51
|
-
const lastPhase = phaseWatch[name];
|
|
52
|
-
|
|
53
|
-
// Always stored, so the alert fires once per transition.
|
|
54
|
-
try {
|
|
55
|
-
phaseWatch[name] = currentPhase;
|
|
56
|
-
writeFileSync(watchPath, JSON.stringify(phaseWatch, null, 2), 'utf-8');
|
|
57
|
-
} catch {}
|
|
58
|
-
|
|
59
|
-
const isPhaseTransition = lastPhase && lastPhase !== currentPhase;
|
|
60
|
-
const enteringImplement = COMPACT_TRIGGER_PHASES.has(currentPhase);
|
|
61
|
-
const comingFromHeavyPhase = HEAVY_SOURCE_PHASES.has(lastPhase);
|
|
62
|
-
|
|
63
|
-
if (isPhaseTransition && enteringImplement && comingFromHeavyPhase) {
|
|
64
|
-
warnings.push(
|
|
65
|
-
`⚡ Phase advanced: ${lastPhase} → implement`,
|
|
66
|
-
``,
|
|
67
|
-
`💡 Recommended before starting implementation:`,
|
|
68
|
-
` Run /compact to free up context — plan.md and the design`,
|
|
69
|
-
` loaded during planning take significant space. A compact now gives`,
|
|
70
|
-
` Claude a clean slate focused on writing code.`,
|
|
71
|
-
``,
|
|
72
|
-
` After /compact, resume with: /morph-apply ${name}`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
41
|
// Hash-gated: the audit re-runs every Stop, so only a changed warning set is injected; resolving clears the hash.
|
|
77
42
|
try {
|
|
78
43
|
const featurePath = join(cwd, '.morph', 'features', name);
|
|
@@ -187,23 +187,40 @@ export function decideIsolation({ findings = [], insideWorktree = false } = {})
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// HEAD alone would let a stack built before an uncommitted edit read as fresh: status and diff
|
|
190
|
-
// make any edit move the stamp.
|
|
190
|
+
// make any edit move the stamp. `.morph/` stays out, config.json aside: the verify's own commits
|
|
191
|
+
// must not rebuild a stack whose source did not change. `null` outside a git repo.
|
|
191
192
|
export function computeSourceStamp(cwd, exec = defaultExec) {
|
|
192
193
|
const head = exec('git', ['rev-parse', 'HEAD'], { cwd });
|
|
193
194
|
if (head.code !== 0) return null;
|
|
194
195
|
|
|
195
|
-
const
|
|
196
|
-
const
|
|
196
|
+
const tree = exec('git', ['ls-tree', 'HEAD'], { cwd });
|
|
197
|
+
const status = exec('git', ['status', '--porcelain', ...OUTSIDE_MORPH], { cwd });
|
|
198
|
+
const diff = exec('git', ['diff', 'HEAD', ...OUTSIDE_MORPH], { cwd });
|
|
199
|
+
const entries = tree.code === 0
|
|
200
|
+
? tree.output.split('\n').filter((line) => line.split('\t')[1] !== '.morph').join('\n')
|
|
201
|
+
: head.output.trim();
|
|
197
202
|
|
|
198
203
|
return createHash('sha1')
|
|
199
|
-
.update(
|
|
204
|
+
.update(entries)
|
|
200
205
|
.update('\0')
|
|
201
206
|
.update(status.code === 0 ? status.output : '')
|
|
202
207
|
.update('\0')
|
|
203
208
|
.update(diff.code === 0 ? diff.output : '')
|
|
209
|
+
.update('\0')
|
|
210
|
+
.update(readConfigText(cwd))
|
|
204
211
|
.digest('hex');
|
|
205
212
|
}
|
|
206
213
|
|
|
214
|
+
const OUTSIDE_MORPH = ['--', '.', ':(exclude).morph'];
|
|
215
|
+
|
|
216
|
+
function readConfigText(cwd) {
|
|
217
|
+
try {
|
|
218
|
+
return readFileSync(join(cwd, '.morph', 'config', 'config.json'), 'utf8');
|
|
219
|
+
} catch {
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
207
224
|
export function stampPath(gitCommonDir, projectName) {
|
|
208
225
|
return join(gitCommonDir, 'morph-spec', `e2e-stamp-${projectName}.json`);
|
|
209
226
|
}
|
|
@@ -775,6 +792,11 @@ export function bringUpE2e(
|
|
|
775
792
|
return result({ status: 'pass', stage: null, reason: stack.reason, hostServices: host, auth });
|
|
776
793
|
}
|
|
777
794
|
|
|
795
|
+
function specTimeoutFor(config) {
|
|
796
|
+
const configured = config?.e2e?.specTimeoutMs;
|
|
797
|
+
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_SPEC_TIMEOUT;
|
|
798
|
+
}
|
|
799
|
+
|
|
778
800
|
/** @returns {{ status: 'pass'|'fail'|'skip', command: string|null, output: string, reason: string|null, warnings: string[] }} */
|
|
779
801
|
export function runE2eCheck(cwd, options = {}) {
|
|
780
802
|
const {
|
|
@@ -787,7 +809,7 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
787
809
|
forceFresh = false,
|
|
788
810
|
insideWorktree = isSecondaryWorktree(cwd),
|
|
789
811
|
healthTimeout = DEFAULT_HEALTH_TIMEOUT,
|
|
790
|
-
specTimeout =
|
|
812
|
+
specTimeout = specTimeoutFor(config),
|
|
791
813
|
beforeHeavy = null,
|
|
792
814
|
countSpecs = null,
|
|
793
815
|
_exec: exec = defaultExec,
|
|
@@ -858,6 +880,12 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
858
880
|
}
|
|
859
881
|
}
|
|
860
882
|
|
|
883
|
+
// Playwright's list reporter names a failure as ` 1) [chromium] › e2e/a.spec.ts:3:5 › title`.
|
|
884
|
+
function failedSpecNames(output) {
|
|
885
|
+
const names = [...String(output).matchAll(/^\s+\d+\)\s+(.+›.+)$/gm)].map((match) => match[1].trim());
|
|
886
|
+
return [...new Set(names)];
|
|
887
|
+
}
|
|
888
|
+
|
|
861
889
|
// The suite gets the stack's env (with PLAYWRIGHT_BASE_URL): without it the config hits a hardcoded
|
|
862
890
|
// localhost:3000 while the app sits on the feature's port, an ECONNREFUSED that looks like a broken app.
|
|
863
891
|
function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv, countSpecs }, exec) {
|
|
@@ -885,18 +913,27 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv,
|
|
|
885
913
|
// Counted from the FULL output, before the tail cut; unparseable is null, never 0.
|
|
886
914
|
const count = typeof countSpecs === 'function' ? (countSpecs(run.output) ?? null) : null;
|
|
887
915
|
|
|
888
|
-
|
|
916
|
+
// One rerun of the failed specs only: a flake must not cost a whole verify. `--last-failed`
|
|
917
|
+
// needs Playwright 1.44+; an older one exits non-zero, and the node stays red.
|
|
918
|
+
const retry = run.code !== 0 && spec.args.includes('playwright')
|
|
919
|
+
? exec(spec.command, [...spec.args, '--last-failed'], { cwd: spec.cwd, timeout: specTimeout, env: composeEnv })
|
|
920
|
+
: null;
|
|
921
|
+
|
|
922
|
+
if (run.code !== 0 && retry?.code !== 0) {
|
|
889
923
|
return {
|
|
890
924
|
status: 'fail',
|
|
891
925
|
command: commandLine,
|
|
892
|
-
output: tailLines(run.output, OUTPUT_TAIL_LINES),
|
|
926
|
+
output: tailLines(retry?.output ?? run.output, OUTPUT_TAIL_LINES),
|
|
893
927
|
reason: 'e2e specs failed against the running stack',
|
|
894
928
|
count,
|
|
895
929
|
};
|
|
896
930
|
}
|
|
897
931
|
|
|
932
|
+
const flakySpecs = retry ? failedSpecNames(run.output) : [];
|
|
933
|
+
|
|
898
934
|
return {
|
|
899
935
|
status: 'pass',
|
|
936
|
+
flakySpecs,
|
|
900
937
|
command: commandLine,
|
|
901
938
|
output: '',
|
|
902
939
|
reason: notes.length ? notes.join('; ') : null,
|
|
@@ -57,6 +57,14 @@ function firstArg(args, valueFlags = new Set()) {
|
|
|
57
57
|
return undefined;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const POWERSHELL_HOSTS = new Set(['powershell', 'pwsh']);
|
|
61
|
+
const TEST_SCRIPT_SUFFIX = 'test-script';
|
|
62
|
+
|
|
63
|
+
function runsTestScript(args) {
|
|
64
|
+
const script = args.find((a) => /\.ps1$/i.test(a));
|
|
65
|
+
return Boolean(script) && /test/i.test(script.replace(/^.*[\\/]/, ''));
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
function kindOfTokens(tokens) {
|
|
61
69
|
const [head, ...args] = tokens;
|
|
62
70
|
if (!head || args.some((a) => INFO_FLAGS.has(a))) return null;
|
|
@@ -64,6 +72,7 @@ function kindOfTokens(tokens) {
|
|
|
64
72
|
const sub = firstArg(args, exe === 'docker' || exe === 'docker-compose' ? DOCKER_VALUE_FLAGS
|
|
65
73
|
: PACKAGE_MANAGERS.has(exe) ? PM_DIR_FLAGS : undefined);
|
|
66
74
|
|
|
75
|
+
if (POWERSHELL_HOSTS.has(exe)) return runsTestScript(args) ? `${exe} ${TEST_SCRIPT_SUFFIX}` : null;
|
|
67
76
|
if (exe === 'dotnet') return DOTNET_HEAVY.has(sub) ? `dotnet ${sub}` : null;
|
|
68
77
|
if (exe === 'msbuild') return 'msbuild';
|
|
69
78
|
if (exe === 'vitest' || exe === 'jest') return exe;
|
|
@@ -115,7 +124,7 @@ export const REVIEW_AGENT_TYPES = new Set(['morph-verifier', 'morph-reviewer', '
|
|
|
115
124
|
export const QUEUE_FULL_EXIT = 3;
|
|
116
125
|
|
|
117
126
|
const LIST_FLAGS = new Set(['--list', '--listTests', '--list-tests']);
|
|
118
|
-
const TEST_LABEL = /^(dotnet (test|vstest)|(npx )?(vitest|jest|playwright test)|(npm|pnpm|yarn|bun) (test|run test(:\S+)?))$/;
|
|
127
|
+
const TEST_LABEL = /^(dotnet (test|vstest)|(npx )?(vitest|jest|playwright test)|(npm|pnpm|yarn|bun) (test|run test(:\S+)?)|(powershell|pwsh) test-script)$/;
|
|
119
128
|
const NODE_RUNNERS = new Set(['vitest', 'jest']);
|
|
120
129
|
|
|
121
130
|
function morphArgs(tokens) {
|
|
@@ -158,6 +167,8 @@ const TEST_PATH = /[\\/]|\.(spec|test)\b|\.[cm]?[jt]sx?$/;
|
|
|
158
167
|
const PATH_VALUE_FLAGS = new Set(['-c', '--config', '--project', '--root', '--workspace', '--outputFile', '--reporter', ...PM_DIR_FLAGS]);
|
|
159
168
|
|
|
160
169
|
function isFilteredRun(kind, tokens) {
|
|
170
|
+
// A script's arguments are its own contract: no flag here proves it narrowed the run.
|
|
171
|
+
if (kind.endsWith(TEST_SCRIPT_SUFFIX)) return false;
|
|
161
172
|
const runnerAt = executable(tokens[0]) === 'node'
|
|
162
173
|
? tokens.findIndex((t, i) => i > 0 && executable(t).replace(/\.[cm]?js$/, '') === kind)
|
|
163
174
|
: 0;
|
|
@@ -26,14 +26,14 @@ const HOW = {
|
|
|
26
26
|
+ `em 3-implement/report.md e commite \`wip(${f}): Cn …\`.`,
|
|
27
27
|
],
|
|
28
28
|
decide: () => [
|
|
29
|
-
'Leia a pergunta do Cn bloqueado em 3-implement/report.md ## Notas; decida pelo plano/ADR ou faça AskUserQuestion com ela.',
|
|
30
|
-
'Registre a resposta em ## Notas, volte o Cn para `pendente` e siga implementando
|
|
29
|
+
'Leia a pergunta do Cn bloqueado em 3-implement/report.md ## Notas; decida pelo plano/ADR ou faça AskUserQuestion com ela. Se o `Resultado buscado` ficou falso, o plano não decide: pergunte.',
|
|
30
|
+
'Registre a resposta em ## Notas, volte o Cn para `pendente` e siga implementando; resposta que muda escopo vai por `unapprove`.',
|
|
31
31
|
],
|
|
32
32
|
verify: (f) => [
|
|
33
33
|
`\`morph-spec verify ${f} --json\` sem filtro; passou de 10 min, run_in_background.`,
|
|
34
34
|
],
|
|
35
35
|
fix: (f) => [
|
|
36
|
-
`Leia o \`feedback\` do último verify, corrija a causa e rode \`morph-spec verify ${f} --json\` de novo, sem filtro.`,
|
|
36
|
+
`Leia o \`feedback\` do último verify, corrija a causa e rode \`morph-spec verify ${f} --json\` de novo, sem filtro: ele começa pelas suítes vermelhas e só segue se passarem.`,
|
|
37
37
|
],
|
|
38
38
|
escalate: () => [
|
|
39
39
|
'AskUserQuestion com o texto do reason (aceitar/continuar/parar) e siga a resposta; não rode outro verify antes dela.',
|
|
@@ -42,7 +42,7 @@ const HOW = {
|
|
|
42
42
|
'Siga .claude/skills/morph-apply/references/review.md.',
|
|
43
43
|
],
|
|
44
44
|
finish: (f) => [
|
|
45
|
-
|
|
45
|
+
`\`morph-spec context refresh\`, commit, e \`morph-spec finish ${f} --pr\` (\`--merge\` com \`finish.mode: "merge"\` no config), sem perguntar.`,
|
|
46
46
|
],
|
|
47
47
|
status: (f) => [
|
|
48
48
|
`Rode \`morph-spec status ${f}\` e confira a fase em .morph/features/${f}/feature.json.`,
|
|
@@ -252,11 +252,11 @@
|
|
|
252
252
|
},
|
|
253
253
|
"neon": {
|
|
254
254
|
"phases": ["plan", "implement"],
|
|
255
|
-
"tools": ["list_projects", "describe_table_schema", "get_database_tables", "run_sql", "explain_sql_statement", "prepare_database_migration", "complete_database_migration", "compare_database_schema", "create_branch", "list_branches", "get_connection_string"],
|
|
255
|
+
"tools": ["list_projects", "describe_table_schema", "get_database_tables", "run_sql", "explain_sql_statement", "prepare_database_migration", "complete_database_migration", "compare_database_schema", "create_branch", "list_branches", "get_connection_string", "list_slow_queries", "prepare_query_tuning", "complete_query_tuning", "create_snapshot", "search_docs", "get_doc"],
|
|
256
256
|
"serverNames": ["neon", "Neon"],
|
|
257
257
|
"detection": "mcpServers includes neon (case-insensitive)",
|
|
258
|
-
"fallback": "SQL via the connection string (psql / dotnet ef) and the
|
|
259
|
-
"usage": "Schema inspection, migration dry-run on a throwaway branch (prepare_database_migration → complete_database_migration), query tuning. The server exposes DESTRUCTIVE tools (delete_branch, delete_project, reset_from_parent): never call one without explicit human confirmation",
|
|
258
|
+
"fallback": "SQL via the connection string (psql / dotnet ef) and the neon CLI for branches",
|
|
259
|
+
"usage": "Schema inspection, migration dry-run on a throwaway branch (prepare_database_migration → complete_database_migration), query tuning (list_slow_queries, prepare_query_tuning), snapshot before a risky migration, current Neon docs (search_docs, get_doc). Work on a branch: the Neon docs say never to connect MCP agents to production databases. The server exposes DESTRUCTIVE tools (delete_branch, delete_project, reset_from_parent): never call one without explicit human confirmation",
|
|
260
260
|
"healthCheck": {
|
|
261
261
|
"toolPattern": "neon",
|
|
262
262
|
"testCall": "Call list_projects with limit 1",
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
"credentials": [],
|
|
275
275
|
"prerequisites": [],
|
|
276
276
|
"stackRelevance": ["neon"],
|
|
277
|
-
"warnings": ["OAuth against your Neon account on first use. The server can delete branches and projects: keep destructive calls behind a human."],
|
|
277
|
+
"warnings": ["OAuth against your Neon account on first use. The server can delete branches and projects: keep destructive calls behind a human, and point agents at a branch, never at production."],
|
|
278
278
|
"setupGuide": "Run: claude mcp add --transport http neon https://mcp.neon.tech/mcp"
|
|
279
279
|
}
|
|
280
280
|
},
|
package/framework/phases.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"displayName": "Plan",
|
|
9
9
|
"goals": [
|
|
10
10
|
"Understand the business problem and scan the stack (brownfield) or assume the default (greenfield)",
|
|
11
|
-
"Write plan.md — scenarios with proof, decisions
|
|
11
|
+
"Write plan.md — scenarios with proof, decisions and reuse",
|
|
12
12
|
"Run plan-check to produce checks.json and reach the plan gate"
|
|
13
13
|
],
|
|
14
14
|
"requiredOutputs": [
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"folder": "3-implement",
|
|
53
53
|
"displayName": "Implementation",
|
|
54
54
|
"goals": [
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"One
|
|
55
|
+
"Implement the whole plan in this session, in sequence",
|
|
56
|
+
"Mark a scenario `feito` only with its evidence; close the phase on a green, unfiltered verify",
|
|
57
|
+
"One row per scenario in 3-implement/report.md — the implementation record"
|
|
58
58
|
],
|
|
59
59
|
"requiredOutputs": [],
|
|
60
60
|
"optionalOutputs": [
|
|
@@ -26,7 +26,7 @@ stacks:
|
|
|
26
26
|
- **ai-agents-structured-output** (`ai-agents/structured-output.md`) — Saída que alimenta outro sistema é record tipado (`RunAsync<T>` ou `ChatResponseFormat.ForJsonSchema<T>()`): `sealed record` com `[Description]` e `[JsonPropertyName]`. Raiz nunca primitivo/array (use wrapper); em streaming, `ToAgentResponseAsync()` antes de desserializar. Regra de negócio se valida depois da chamada.
|
|
27
27
|
- **ai-agents-multi-agent-patterns** (`ai-agents/multi-agent-patterns.md`) — Do mais barato: agent-as-tool (`AsAIFunction()`, aninhamento ≤ 2) → sequencial → workflow; handoff vem depois de agent-as-tool, com Id estável e `WithAutonomousMode(turnLimit:)`; group chat ≤ 3 agentes. PROIBIDO: agente auxiliar DENTRO de uma tool (dois loops de function calling no mesmo turno).
|
|
28
28
|
- **ai-agents-prompt-sources** (`ai-agents/prompt-sources.md`) — Prompt fixo: tabela versionada `agent_prompts` (INSERT + flip de `is_active` numa transação, nunca UPDATE; id `Guid.CreateVersion7()`). Prompt por tenant: YAML no repo → compositor determinístico → spec renderizada em jsonb. O registro de execução grava a versão EFETIVA (e o hash do blob no caminho composto).
|
|
29
|
-
- **ai-agents-rag-custom-pgvector** (`ai-agents/rag-custom-pgvector.md`) — RAG em pgvector via EF Core: `IEmbeddingGenerator` injetado direto (sem EmbeddingService), alias `embeddings` do registry, `CosineDistance` LINQ, HNSW `vector_cosine_ops`, `tenant_id` filtrado em toda query e nunca exposto ao LLM. Ingestão delta por lookup-key: Unchanged nunca re-embeda, Removed só depois das escritas.
|
|
29
|
+
- **ai-agents-rag-custom-pgvector** (`ai-agents/rag-custom-pgvector.md`) — RAG em pgvector via EF Core: `IEmbeddingGenerator` injetado direto (sem EmbeddingService), alias `embeddings` do registry, `CosineDistance` LINQ, HNSW `vector_cosine_ops`, `tenant_id` filtrado em toda query e nunca exposto ao LLM. Ingestão delta por lookup-key: Unchanged nunca re-embeda, Removed só depois das escritas. Corpus com código, SKU ou nome próprio: busca híbrida com RRF (`infrastructure-neon-pgvector#hybrid-search-vector--full-text`).
|
|
30
30
|
- **ai-agents-middleware-patterns** (`ai-agents/middleware-patterns.md`) — Registro sempre `.AsBuilder().Use(...).Build()`; o PRIMEIRO registrado é o mais externo. Ordem medida: timeout → usage-cost → FunctionInvokingChatClient → chat telemetry → provider. Só o delegate não-streaming rebaixa streaming em silêncio: use `Use(sharedFunc)`.
|
|
31
31
|
- **ai-agents-observability-patterns** (`ai-agents/observability-patterns.md`) — Registre os DOIS ActivitySources (`Experimental.Microsoft.Agents.AI` e `Experimental.Microsoft.Extensions.AI`); a API é `builder.UseOpenTelemetry(...)` (`agent.WithOpenTelemetry()` não existe). Instrumentar os dois níveis é normal; o mesmo nível duas vezes duplica. Prompt cru só em dev com opt-in, hash em produção; turno cancelado não grava nada.
|
|
32
32
|
- **ai-agents-workflows** (`ai-agents/workflows.md`) — Raro: só para fan-in/out real ou HITL longo; `foreach` + LINQ costuma bastar. `AgentWorkflowBuilder`/`WorkflowBuilder`/`InProcessExecution` vivem no pacote SEPARADO `Microsoft.Agents.AI.Workflows` 1.20.0. Agente declarativo (`Microsoft.Agents.AI.Declarative`) ainda pede `--prerelease`.
|
|
@@ -13,6 +13,6 @@ stacks:
|
|
|
13
13
|
|
|
14
14
|
- **data-nosql-cache-redis** (`data/nosql/cache/redis.md`) — Cache distribuído, nunca store primário. `Microsoft.Extensions.Caching.StackExchangeRedis` 10.0.12; `IConnectionMultiplexer` singleton LAZY (nunca conectar no registro). Cache-aside no handler com `IDistributedCache` (sem wrapper); invalide a chave exata depois do `SaveChangesAsync`. PROIBIDO: cachear segredo/PII sem criptografia, omitir TTL, engolir `RedisConnectionException`.
|
|
15
15
|
- **data-vector-search-rag-chunking** (`data/vector-search/rag-chunking.md`) — Prosa: sentence-aware, 256 tokens; doc técnico: semântico por headers; Q&A inteiro; código: a função inteira. Overlap 10–20%, mínimo 50 e máximo ~512 tokens. Sempre grave metadados do chunk (DocumentId, ChunkIndex, Section, SourceUrl) e reindexe quando o hash do conteúdo mudar.
|
|
16
|
-
- **backend-database-vector-search-rag** (`backend/database/vector-search-rag.md`) — Slices VSA sobre EF Core 10 + pgvector: o handler injeta `ApplicationDbContext` e `IEmbeddingGenerator` direto (sem EmbeddingService/SearchService). Busca por `d.Embedding!.CosineDistance(query)` com a distância em ordem ASC. Sem documento relevante, `NotFound` antes de chamar o LLM. Indexação em lote: um `GenerateAsync` e um `SaveChanges`.
|
|
16
|
+
- **backend-database-vector-search-rag** (`backend/database/vector-search-rag.md`) — Slices VSA sobre EF Core 10 + pgvector: o handler injeta `ApplicationDbContext` e `IEmbeddingGenerator` direto (sem EmbeddingService/SearchService). Busca por `d.Embedding!.CosineDistance(query)` com a distância em ordem ASC. Sem documento relevante, `NotFound` antes de chamar o LLM. Indexação em lote: um `GenerateAsync` e um `SaveChanges`. Corpus com código, SKU ou nome próprio: busca híbrida com RRF (`infrastructure-neon-pgvector#hybrid-search-vector--full-text`).
|
|
17
17
|
|
|
18
18
|
> Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.
|
|
@@ -10,6 +10,6 @@ stacks:
|
|
|
10
10
|
|
|
11
11
|
# Hangfire Background Jobs Standards
|
|
12
12
|
|
|
13
|
-
- **backend-integrations-hangfire-hangfire-jobs** (`backend/integrations/hangfire/hangfire-jobs.md`) — Hangfire 1.8.25 só com razão real em `plan.md` (`## Decisões`); o que cabe no request fica no handler. Storage `Hangfire.PostgreSql` 1.21.1 com `UseNpgsqlConnection` no endpoint DIRETO do Neon; referencie `Newtonsoft.Json` 13.0.4. Job é classe fina e idempotente que chama o handler; `[AutomaticRetry]`, sem try/catch nem retry manual. Dashboard exige Admin autenticado.
|
|
13
|
+
- **backend-integrations-hangfire-hangfire-jobs** (`backend/integrations/hangfire/hangfire-jobs.md`) — Hangfire 1.8.25 só com razão real em `plan.md` (`## Decisões`); o que cabe no request fica no handler. Tarefa genérica e cron usam Hangfire; job com estado que a app lê usa tabela + claim `SKIP LOCKED` (`backend-dotnet-vsa-handler-patterns#queue-on-postgres`). Storage `Hangfire.PostgreSql` 1.21.1 com `UseNpgsqlConnection` no endpoint DIRETO do Neon; referencie `Newtonsoft.Json` 13.0.4. Job é classe fina e idempotente que chama o handler; `[AutomaticRetry]`, sem try/catch nem retry manual. Dashboard exige Admin autenticado.
|
|
14
14
|
|
|
15
15
|
> Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.
|
|
@@ -10,6 +10,6 @@ stacks:
|
|
|
10
10
|
|
|
11
11
|
# Neon Auth Standards
|
|
12
12
|
|
|
13
|
-
- **backend-integrations-neon-auth-neon-auth** (`backend/integrations/neon-auth/neon-auth.md`) — SDK `@neondatabase/auth
|
|
13
|
+
- **backend-integrations-neon-auth-neon-auth** (`backend/integrations/neon-auth/neon-auth.md`) — SDK `@neon/auth` (≥ 0.8.1; `@neondatabase/auth` é o nome antigo): `createNeonAuth()` no server, `createAuthClient()` no client, `auth.middleware()` com `runtime='nodejs'`. Duas constantes de prefixo (`BACKEND_API_PREFIX`/`PROXY_API_PREFIX`), nunca uma só. O front não consulta o Neon para dado de negócio. .NET: token EdDSA/Ed25519, verificado pelo JWKS com BouncyCastle + `SignatureValidator` devolvendo `JsonWebToken`.
|
|
14
14
|
|
|
15
15
|
> Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.
|
|
@@ -11,9 +11,9 @@ stacks:
|
|
|
11
11
|
# Neon PostgreSQL Standards
|
|
12
12
|
|
|
13
13
|
- **infrastructure-neon-setup** (`infrastructure/neon/neon-setup.md`) — Duas connection strings: `DefaultConnection` (pooled `-pooler`) para a app e `MigrationConnection` (direta) para migrations e advisory locks; nunca commitadas (user-secrets / env do Coolify). Formato keyword (`Host=...;SSL Mode=Require`), não URI. `EnableRetryOnFailure(5, 10s)` por causa do scale-to-zero. Branch sempre com `--expires-at`.
|
|
14
|
-
- **infrastructure-neon-pgvector** (`infrastructure/neon/neon-pgvector.md`) — `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.3 + `Pgvector.EntityFrameworkCore` 0.3.0; coluna `Vector` (`vector(1536)`) e `UseVector()` no `UseNpgsql`. SEMPRE índice HNSW com operator class casando a função LINQ (senão seq scan); distância em ordem ASC. PROIBIDO: DocumentRepository, SQL cru do C# sem `plan.md` (`## Decisões`), default `gen_random_uuid()`.
|
|
15
|
-
- **infrastructure-neon-
|
|
16
|
-
- **backend-integrations-neon-auth-neon-auth** (`backend/integrations/neon-auth/neon-auth.md`) — SDK `@neondatabase/auth
|
|
14
|
+
- **infrastructure-neon-pgvector** (`infrastructure/neon/neon-pgvector.md`) — `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.3 + `Pgvector.EntityFrameworkCore` 0.3.0; coluna `Vector` (`vector(1536)`) e `UseVector()` no `UseNpgsql`. SEMPRE índice HNSW com operator class casando a função LINQ (senão seq scan); distância em ordem ASC. PROIBIDO: DocumentRepository, SQL cru do C# sem `plan.md` (`## Decisões`), default `gen_random_uuid()`. Busca híbrida: `tsvector` gerado + GIN + pgvector fundidos por RRF, nunca soma de scores. No Neon, `pg_search` saiu, `pgrag` está deprecado, `pgmq` e Apache AGE estão fora do catálogo; `lakebase_text` só com ADR.
|
|
15
|
+
- **infrastructure-neon-platform** (`infrastructure/neon/neon-platform.md`) — O que o Neon já entrega: branch descartável, schema diff, snapshot antes de migration com `degrau:`, Object Storage, AI Gateway, diagnóstico de consulta, read replica. A região limita o catálogo (`sa-east-1` só tem Postgres e Auth). MCP nunca em produção: agente trabalha em branch.
|
|
16
|
+
- **backend-integrations-neon-auth-neon-auth** (`backend/integrations/neon-auth/neon-auth.md`) — SDK `@neon/auth` (≥ 0.8.1; `@neondatabase/auth` é o nome antigo): `createNeonAuth()` no server, `createAuthClient()` no client, `auth.middleware()` com `runtime='nodejs'`. Duas constantes de prefixo (`BACKEND_API_PREFIX`/`PROXY_API_PREFIX`), nunca uma só. O front não consulta o Neon para dado de negócio. .NET: token EdDSA/Ed25519, verificado pelo JWKS com BouncyCastle + `SignatureValidator` devolvendo `JsonWebToken`.
|
|
17
17
|
|
|
18
18
|
> Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.
|
|
19
19
|
|