@polymorphism-tech/morph-spec 9.1.0 → 9.3.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 (104) hide show
  1. package/CLAUDE.md +14 -78
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +14 -1
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +4 -3
  8. package/framework/MORPH.md +7 -10
  9. package/framework/agents.json +15 -12
  10. package/framework/commands/morph-status.md +2 -2
  11. package/framework/docs/CLI.md +24 -16
  12. package/framework/evals/review-checklist.md +15 -2
  13. package/framework/hooks/README.md +8 -44
  14. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  15. package/framework/hooks/claude-code/core/state-sync.js +1 -1
  16. package/framework/hooks/claude-code/core/worktree-context.js +23 -0
  17. package/framework/hooks/claude-code/statusline.py +3 -2
  18. package/framework/hooks/claude-code/statusline.sh +11 -11
  19. package/framework/hooks/shared/gate-event.js +18 -0
  20. package/framework/hooks/shared/next-step.js +107 -0
  21. package/framework/hooks/shared/slice-progress.js +18 -5
  22. package/framework/hooks/shared/state-writer.js +6 -1
  23. package/framework/mcp/registry.json +31 -1
  24. package/framework/rules/nextjs-standards.md +1 -1
  25. package/framework/schemas/feature.schema.json +1 -0
  26. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  27. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  28. package/framework/skills/core/morph-apply/SKILL.md +6 -4
  29. package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
  30. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  31. package/framework/skills/core/morph-media/SKILL.md +1 -1
  32. package/framework/skills/core/morph-plan/SKILL.md +8 -6
  33. package/framework/skills/core/morph-review/SKILL.md +9 -6
  34. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  35. package/framework/skills/core/morph-uiux/SKILL.md +2 -1
  36. package/framework/standards/STANDARDS.json +2 -2
  37. package/framework/standards/STANDARDS.meta.json +1 -1
  38. package/framework/standards/ai-agents/setup.md +2 -0
  39. package/framework/standards/backend/dotnet/testing.md +10 -4
  40. package/framework/standards/frontend/nextjs/testing.md +10 -6
  41. package/framework/standards/integration/mcp/mcp-tools.md +3 -1
  42. package/framework/templates/.idea/morph-templates.xml +92 -0
  43. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  44. package/framework/templates/IDE-SNIPPETS.md +262 -262
  45. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  46. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  47. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  48. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  49. package/framework/templates/docs/plan.md +3 -1
  50. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  51. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  52. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  53. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  54. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  55. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  56. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  57. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  58. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  59. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  60. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  61. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  62. package/framework/templates/state.template.json +17 -17
  63. package/framework/templates/ui/design-system.css +226 -226
  64. package/package.json +1 -1
  65. package/src/commands/observability/cost.js +12 -0
  66. package/src/commands/project/status.js +6 -68
  67. package/src/commands/state/advance.js +1 -1
  68. package/src/commands/state/approve.js +2 -2
  69. package/src/commands/state/gate-check.js +2 -2
  70. package/src/commands/state/gate-decision.js +4 -1
  71. package/src/commands/state/next.js +24 -0
  72. package/src/commands/state/report-check.js +3 -1
  73. package/src/commands/validation/mutate.js +9 -6
  74. package/src/commands/validation/plan-check.js +4 -2
  75. package/src/commands/validation/tests-audit.js +7 -0
  76. package/src/commands/validation/validate-feature.js +4 -2
  77. package/src/commands/validation/verify.js +21 -16
  78. package/src/core/state/state-manager.js +4 -0
  79. package/src/dashboard/app/main.jsx +13 -13
  80. package/src/dashboard/app/pages/FeatureDetail.jsx +6 -2
  81. package/src/dashboard/scanner/features-scanner.js +5 -2
  82. package/src/dashboard/vite.config.js +24 -24
  83. package/src/lib/ci/workflow-scaffold.js +12 -0
  84. package/src/lib/e2e/applicability.js +37 -0
  85. package/src/lib/generators/handoff-generator.js +11 -25
  86. package/src/lib/mutation/adapters.js +6 -17
  87. package/src/lib/mutation/runner.js +23 -75
  88. package/src/lib/mutation/trx.js +0 -1
  89. package/src/lib/next-step.js +10 -0
  90. package/src/lib/plan/checks.js +29 -2
  91. package/src/lib/plan/plan-parser.js +6 -2
  92. package/src/lib/review-reports.js +6 -3
  93. package/src/lib/risk-detector.js +20 -7
  94. package/src/lib/stack/stack-scan.js +13 -1
  95. package/src/lib/telemetry/metrics.js +46 -0
  96. package/src/lib/tests-audit/audit.js +96 -0
  97. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  98. package/src/lib/validators/validation-runner.js +6 -38
  99. package/src/lib/validators/verify-runner.js +37 -22
  100. package/src/utils/agents-installer.js +24 -1
  101. package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
  102. package/framework/hooks/git/pre-commit/agents.sh +0 -25
  103. package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
  104. package/framework/hooks/git/pre-push/run-tests.sh +0 -44
@@ -0,0 +1,18 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { appendTelemetryEvent } from './telemetry-logger.js';
4
+
5
+ export const GATE_EVENT = 'Gate';
6
+ export const GATE_STEPS = ['decision', 'approve', 'unapprove'];
7
+
8
+ /**
9
+ * The trail `cost --metrics` turns into "what did the human do with each pause":
10
+ * `gateDecisions` keeps only the latest decision, so the history lives here.
11
+ */
12
+ export function recordGateEvent(projectPath, { feature, gate, step, pause, signal, source, approvedBy }) {
13
+ if (!existsSync(join(projectPath || process.cwd(), '.morph'))) return;
14
+ const event = { event: GATE_EVENT, feature, gate, step };
15
+ if (step === 'decision') Object.assign(event, { pause: pause === true, signal: signal ?? null, source: source ?? null });
16
+ else event.approvedBy = approvedBy ?? null;
17
+ appendTelemetryEvent(event, projectPath);
18
+ }
@@ -0,0 +1,107 @@
1
+ // The single rule of "what the session does next". `decideNext` is pure: collectors feed facts.
2
+ // Hooks and the CLI share it so the SessionStart pre-fetch and `morph-spec next` never disagree.
3
+
4
+ import { existsSync, readFileSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { derivePhaseForFeature } from './state-reader.js';
7
+ import { sliceProgress, readySlices } from './slice-progress.js';
8
+
9
+ export const FIX_ATTEMPTS = 3;
10
+
11
+ const step = (action, command, reason, extra = {}) => ({ action, command, reason, ...extra });
12
+
13
+ /**
14
+ * @param {{ feature: string, phase: string, slices: object|null, gates: object,
15
+ * verification: { status?: string }|null, verificationStale?: boolean, failStreak?: number }} s
16
+ * `verificationStale` undefined = unknown (the hook has no git): read as fresh.
17
+ */
18
+ export function decideNext(s) {
19
+ const f = s.feature;
20
+ const approved = (g) => s.gates?.[g]?.approved === true;
21
+ // The hotfix pipeline (scout, human approval, ship) lives in its own skill, end to end.
22
+ const skill = (name) => (s.workType === 'hotfix' ? `/morph-hotfix ${f}` : `/${name} ${f}`);
23
+
24
+ switch (s.phase) {
25
+ case 'plan':
26
+ return s.slices
27
+ ? step('gate', `morph-spec gate-check ${f} plan`, 'plano extraído: o gate do plano decide a pausa')
28
+ : step('plan', `/morph-plan ${f}`, 'sem checks.json: escreva o plan.md e rode plan-check até zero erro');
29
+
30
+ case 'uiux':
31
+ return step('design', `morph-spec gate-check ${f} uiux`, 'complete 1-design/ (morph-uiux) e passe o gate de uiux');
32
+
33
+ case 'implement': {
34
+ if (!s.slices) return step('plan', skill('morph-plan'), 'sem checks.json: plan.md curto (uma fatia) e plan-check até zero erro');
35
+ if (s.slices.done < s.slices.total) {
36
+ const ready = readySlices(s.slices);
37
+ if (!ready.length) {
38
+ const waiting = s.slices.slices.filter((sl) => sl.status === 'bloqueado').map((sl) => sl.id);
39
+ if (waiting.length) {
40
+ return step('decide', 'AskUserQuestion (decisão da fatia)',
41
+ 'relatório BLOQUEADO: decida pelo plano/ADR ou pergunte, e retome o MESMO agente com a resposta — nunca re-despache',
42
+ { slices: waiting });
43
+ }
44
+ return step('blocked', `morph-spec report-check ${f}`, 'fatias pendentes sem dependência satisfeita');
45
+ }
46
+ const partial = ready.filter((sl) => sl.status === 'parcial').map((sl) => sl.id);
47
+ return step('dispatch', skill('morph-apply'),
48
+ partial.length ? `retome ${partial.join(', ')} (PARCIAL) e despache as prontas` : 'despache as fatias prontas',
49
+ { slices: ready.map((sl) => sl.id) });
50
+ }
51
+ const v = s.verification;
52
+ if (!v?.status || s.verificationStale === true) {
53
+ return step('verify', `morph-spec verify ${f}`, v?.status ? 'a árvore andou desde o último verify' : 'todas as fatias relatadas, sem verify');
54
+ }
55
+ if (v.status === 'fail') {
56
+ const streak = s.failStreak ?? 0;
57
+ if (streak > 0 && streak % FIX_ATTEMPTS === 0) {
58
+ return step('escalate', 'AskUserQuestion (aceitar/continuar/parar)',
59
+ `${streak} verifies vermelhos seguidos, nunca uma tentativa a mais por conta própria. `
60
+ + `aceitar → ${skill('morph-review')} (o verify vermelho pausa o gate: o humano assina lá); `
61
+ + 'continuar → corrija e rode o verify de novo; parar → encerre aqui. Segue `escalate` até a árvore andar');
62
+ }
63
+ return step('fix', `morph-spec verify ${f}`,
64
+ `corrija pelo feedback do verify e rode de novo (${FIX_ATTEMPTS - (streak % FIX_ATTEMPTS)} tentativa(s) antes de perguntar)`);
65
+ }
66
+ return step('review', skill('morph-review'), 'verify verde: Gate 3');
67
+ }
68
+
69
+ case 'review':
70
+ return approved('review')
71
+ ? step('finish', `morph-spec finish ${f} --pr|--merge`, 'Gate 3 aprovado: arquiva e integra num passo')
72
+ : step('review', skill('morph-review'), 'relatórios do Gate 3 e gate-check review');
73
+
74
+ default:
75
+ return step('status', `morph-spec status ${f}`, `fase desconhecida: ${s.phase ?? '—'}`);
76
+ }
77
+ }
78
+
79
+ export function formatNext(n) {
80
+ const slices = n.slices?.length ? ` [${n.slices.join(', ')}]` : '';
81
+ return `próximo: ${n.action}${slices} → ${n.command} — ${n.reason}`;
82
+ }
83
+
84
+ /** Facts readable from `hooks/shared`; the CLI adds `verificationStale` (git) on top. Null without feature.json. */
85
+ export function collectNextSignals(featureName, cwd) {
86
+ const featureDir = join(cwd, '.morph', 'features', featureName);
87
+ let feature;
88
+ try {
89
+ const path = join(featureDir, 'feature.json');
90
+ if (!existsSync(path)) return null;
91
+ feature = JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''));
92
+ } catch {
93
+ return null;
94
+ }
95
+ let phase = 'plan';
96
+ try { phase = derivePhaseForFeature(featureName, cwd) || 'plan'; } catch {}
97
+ return {
98
+ feature: featureName,
99
+ workType: feature.workType ?? 'feature',
100
+ worktreePath: feature.worktree?.path ?? null,
101
+ phase,
102
+ slices: sliceProgress(featureDir),
103
+ gates: feature.approvals || feature.approvalGates || {},
104
+ verification: feature.verification ?? null,
105
+ failStreak: feature.verifications?.feature?.failStreak ?? 0,
106
+ };
107
+ }
@@ -4,7 +4,10 @@ import { join } from 'path';
4
4
  /** `PARCIAL` on the report's first non-blank line, tolerating heading/bold/quote typography. */
5
5
  export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
6
6
 
7
- export const SLICE_STATUSES = ['pendente', 'parcial', 'feito'];
7
+ /** `BLOQUEADO`: the slice waits on a decision only the human can make; re-dispatching it repeats the question. */
8
+ export const BLOCKED_MARKER = /^[\s#>*_]*BLOQUEADO\b/;
9
+
10
+ export const SLICE_STATUSES = ['pendente', 'parcial', 'bloqueado', 'feito'];
8
11
 
9
12
  export function checksPath(featureDir) {
10
13
  return join(featureDir, '2-plan', 'checks.json');
@@ -17,10 +20,11 @@ export function sliceReportPath(featureDir, sliceId) {
17
20
  export function sliceStatusFromReport(text) {
18
21
  if (text == null) return 'pendente';
19
22
  const first = String(text).split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
23
+ if (BLOCKED_MARKER.test(first)) return 'bloqueado';
20
24
  return PARTIAL_MARKER.test(first) ? 'parcial' : 'feito';
21
25
  }
22
26
 
23
- /** @returns {{ total, done, partial, pending, slices } | null} null when the checks text is unreadable */
27
+ /** @returns {{ total, done, partial, blocked, pending, slices } | null} null when the checks text is unreadable */
24
28
  export function sliceProgressFrom(checksText, readReport) {
25
29
  let slices;
26
30
  try {
@@ -39,7 +43,14 @@ export function sliceProgressFrom(checksText, readReport) {
39
43
  status: sliceStatusFromReport(readReport(s.id)),
40
44
  }));
41
45
  const count = (status) => out.filter((s) => s.status === status).length;
42
- return { total: out.length, done: count('feito'), partial: count('parcial'), pending: count('pendente'), slices: out };
46
+ return {
47
+ total: out.length,
48
+ done: count('feito'),
49
+ partial: count('parcial'),
50
+ blocked: count('bloqueado'),
51
+ pending: count('pendente'),
52
+ slices: out,
53
+ };
43
54
  }
44
55
 
45
56
  function readOrNull(path) {
@@ -57,7 +68,7 @@ export function sliceProgress(featureDir) {
57
68
  export function readySlices(progress) {
58
69
  if (!progress) return [];
59
70
  const done = new Set(progress.slices.filter((s) => s.status === 'feito').map((s) => s.id));
60
- return progress.slices.filter((s) => s.status !== 'feito' && s.depends.every((d) => done.has(d)));
71
+ return progress.slices.filter((s) => s.status !== 'feito' && s.status !== 'bloqueado' && s.depends.every((d) => done.has(d)));
61
72
  }
62
73
 
63
74
  // Loose on purpose: a false "complete" costs a read, a false "incomplete"
@@ -94,6 +105,8 @@ export function inspectReport(path, { covers = [] } = {}) {
94
105
  const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
95
106
  const partial = PARTIAL_MARKER.test(firstLine);
96
107
  if (partial) missing.push('partial');
108
+ const blocked = BLOCKED_MARKER.test(firstLine);
109
+ if (blocked) missing.push('blocked');
97
110
 
98
- return { exists: true, complete: missing.length === 0, missing, partial };
111
+ return { exists: true, complete: missing.length === 0, missing, partial, blocked };
99
112
  }
@@ -4,6 +4,7 @@
4
4
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'fs';
5
5
  import { join, dirname } from 'path';
6
6
  import { clearStateCache } from './state-reader.js';
7
+ import { recordGateEvent } from './gate-event.js';
7
8
 
8
9
  const STATE_FILE = '.morph/state.json';
9
10
  const STATE_VERSION = '5.0.0';
@@ -81,10 +82,14 @@ export function setGateApproved(featureName, gate, meta = {}, projectPath) {
81
82
  ...(meta.selfAssessed ? { selfAssessed: true } : {}),
82
83
  };
83
84
  feature.updatedAt = now;
84
- return persistFeature(featureName, feature, projectPath);
85
+ if (!persistFeature(featureName, feature, projectPath)) return false;
85
86
  } catch {
86
87
  return false;
87
88
  }
89
+ try {
90
+ recordGateEvent(projectPath, { feature: featureName, gate, step: 'approve', approvedBy: meta.approvedBy || 'manual' });
91
+ } catch {}
92
+ return true;
88
93
  }
89
94
 
90
95
  // Stamps identity so a feature born implicitly by a hook is never an anonymous
@@ -277,9 +277,39 @@
277
277
  "warnings": ["OAuth against your Neon account on first use. The server can delete branches and projects: keep destructive calls behind a human."],
278
278
  "setupGuide": "Run: claude mcp add --transport http neon https://mcp.neon.tech/mcp"
279
279
  }
280
+ },
281
+ "microsoft-learn": {
282
+ "phases": ["plan", "implement"],
283
+ "tools": ["microsoft_docs_search", "microsoft_docs_fetch", "microsoft_code_sample_search"],
284
+ "serverNames": ["microsoft-learn", "plugin_microsoft-docs_microsoft-learn"],
285
+ "detection": "mcpServers includes microsoft-learn or enabledPlugins includes microsoft-docs",
286
+ "fallback": "context7, then WebFetch on learn.microsoft.com",
287
+ "usage": "Official, current .NET 10 / ASP.NET Core / EF Core / Microsoft Agent Framework docs and code samples: search first, fetch the page when the excerpt is cut. Use it for any API the model may know from an older version",
288
+ "healthCheck": {
289
+ "toolPattern": "microsoft-learn",
290
+ "testCall": "Call microsoft_docs_search with query 'EF Core 10 what is new'",
291
+ "testParams": {
292
+ "query": "EF Core 10 what is new"
293
+ },
294
+ "successIndicator": "Returns Microsoft Learn excerpts with contentUrl",
295
+ "failureHint": "Microsoft Learn MCP not responding. Re-add it: claude mcp add --transport http microsoft-learn https://learn.microsoft.com/api/mcp"
296
+ },
297
+ "install": {
298
+ "autoInstall": false,
299
+ "transport": "http",
300
+ "config": {
301
+ "type": "http",
302
+ "url": "https://learn.microsoft.com/api/mcp"
303
+ },
304
+ "credentials": [],
305
+ "prerequisites": [],
306
+ "stackRelevance": ["dotnet"],
307
+ "warnings": [],
308
+ "setupGuide": "Run: claude mcp add --transport http microsoft-learn https://learn.microsoft.com/api/mcp"
309
+ }
280
310
  }
281
311
  },
282
- "_phaseMatrixNote": "Keys must be the v5 phases from framework/phases.json. The v4 keys 'design' and 'tasks' both collapsed into 'plan'; 'review' was absent entirely, so a project in review resolved to undefined. Stack-scoped MCPs (asaas, neon) stay out of this matrix on purpose — only universal ones are listed.",
312
+ "_phaseMatrixNote": "Keys must be the v5 phases from framework/phases.json. The v4 keys 'design' and 'tasks' both collapsed into 'plan'; 'review' was absent entirely, so a project in review resolved to undefined. Stack-scoped MCPs (asaas, neon, microsoft-learn) stay out of this matrix on purpose — only universal ones are listed.",
283
313
  "phaseMatrix": {
284
314
  "plan": ["context7", "graphify"],
285
315
  "uiux": ["playwright", "context7", "stitch", "morph-assets"],
@@ -15,7 +15,7 @@ stacks:
15
15
  - **frontend-nextjs-data-fetching** (`frontend/nextjs/data-fetching.md`) — Server Component com `BACKEND_API_PREFIX`; Client Component só pelo proxy same-origin com `PROXY_API_PREFIX`. TanStack Query 5 com key factory por feature e `invalidateQueries({ queryKey })`; toda resposta passa por Zod. PROIBIDO: `useEffect(() => fetch…)` e paginar/filtrar a coleção inteira no browser.
16
16
  - **frontend-nextjs-forms** (`frontend/nextjs/forms.md`) — react-hook-form + Zod (`zodResolver`) + Field do shadcn via `Controller`; tipo por `z.infer`; nunca `useState` por campo. Submit por `useMutation`, erro em `root`. Edição reusa o schema de criação (`.partial()`). Zod 4: `z.email()`.
17
17
  - **frontend-nextjs-state-management** (`frontend/nextjs/state-management.md`) — Estado de servidor no TanStack Query, nunca copiado para `useState`. Página, ordenação, filtros e aba ativa na URL (`searchParams`). Context só para tema, usuário ou locale. NUNCA Zustand/Redux antes de esgotar Server Components + TanStack Query.
18
- - **frontend-nextjs-testing** (`frontend/nextjs/testing.md`) — Vitest + Testing Library; MSW para API; Playwright para e2e. Teste comportamento, não implementação. Orçamento: um teste por pós-condição + um de regressão por bug; cenários do mesmo comportamento em `it.each`; nada de teste que só confere string de copy.
18
+ - **frontend-nextjs-testing** (`frontend/nextjs/testing.md`) — Vitest + Testing Library; MSW para API; Playwright para e2e. Teste comportamento, não implementação. Orçamento por risco: um teste por cenário com `risco:` + o de regressão do bug, componente sem risco é `leitura`/`e2e`; cenários do mesmo comportamento em `it.each`; nada de teste que só confere string de copy.
19
19
  - **frontend-nextjs-nextjs-patterns** (`frontend/nextjs/nextjs-patterns.md`) — Route Handler é BFF: `auth.getSession()` (401 sem sessão) e delega ao .NET; banco NUNCA em route handler. Zod no client e no server (`safeParse`, 400). `strict: true` + `noUncheckedIndexedAccess`; sem `any` em resposta de API. shadcn via `npx shadcn@latest add`.
20
20
  - **frontend-nextjs-motion-patterns** (`frontend/nextjs/motion-patterns.md`) — Motion via `motion/react` (nunca `framer-motion` junto). Variants em `motion/variants.ts` com durations/easings de `@/lib/design-tokens`. `useReducedMotion()` obrigatório; o `layout.tsx` raiz segue Server Component. 3–5 momentos de motion por página; cena guiada por scroll é do GSAP, nunca no mesmo elemento.
21
21
  - **architecture-code-comments** (`architecture/code-comments.md`) — Padrão é nenhum comentário: renomeie, extraia, tipe ou teste antes. Só o porquê que o código não mostra (invariante, ordem, quirk externo com link), até ~3 linhas. PROIBIDO: história de processo (task, UC, ADR, Gate), repetir o código, XML doc/JSDoc que repete a assinatura, banner, `#region`, código comentado, TODO solto. O hook `comment-guard` bloqueia.
@@ -95,6 +95,7 @@
95
95
  "additionalProperties": false
96
96
  },
97
97
  "attempts": { "type": "integer", "minimum": 1 },
98
+ "failStreak": { "type": "integer", "minimum": 0, "description": "Persisted red verifies in a row, reset to 0 by a green one. Read by `morph-spec next` (decideNext in framework/hooks/shared/next-step.js) to turn the fix loop into an escalation every FIX_ATTEMPTS reds. Absent on entries written before the field existed, which is why it is NOT in required." },
98
99
  "updatedAt": { "type": "string", "format": "date-time" }
99
100
  },
100
101
  "additionalProperties": false
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
4
4
  version: core
5
5
  when_to_use: Use quando for iniciar um novo projeto de LP com o template Umbraco genérico
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
4
4
  version: core
5
5
  when_to_use: Use quando o usuário pedir "use /migrate-lp-to-umbraco {projeto}" ou descrever a necessidade de migrar uma LP para Umbraco
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Executa as fatias do plano de uma feature morph-spec — dispatch p
4
4
  version: core
5
5
  when_to_use: Use com o gate do plano aprovado, em "use /morph-apply {feature}" ou "implementar feature"
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -78,7 +78,7 @@ relatório) está no corpo da persona gerada e chega a todo dispatch sem ser col
78
78
  ```
79
79
  dispatch Agent (subagent_type: "morph-{id}") with prompt:
80
80
  Fatia F{n} — <título>
81
- cobre: <cada Cn com o texto literal do cenário e a `prova:`>
81
+ cobre: <cada Cn com o texto literal do cenário, a `prova:` e o `risco:`>
82
82
  arquivos: <os `arquivos:` da fatia>
83
83
  standards: <por id: o digest de `morph-spec standards --find` + as 3-8 regras que ESTA fatia aplica>
84
84
  reuso: <as linhas de `## Reuso` que tocam a fatia — conceito, veredito, alvo>
@@ -108,6 +108,7 @@ relatório, não a mensagem** — a mensagem final pode não chegar ou vir resum
108
108
  | `ok` | siga |
109
109
  | `report-stale` | **não re-despache**: o agente escreveu depois de relatar; leia `observed` e atualize o relatório |
110
110
  | `report-incomplete` + `resume` | `PARCIAL` na 1ª linha = a fatia não está pronta — o `report-check` o acusa como `report-incomplete`. Retome o MESMO agente por `SendMessage`, colando `tree.dirty`, `tree.commits` e o que falta |
111
+ | `report-blocked` + `ask-human` | `BLOQUEADO` na 1ª linha = decisão que não é do agente; a pergunta e as opções estão no relatório. **Não re-despache.** Plano ou ADR já decide → decida e registre; senão `AskUserQuestion`. Retome o MESMO agente com a resposta |
111
112
  | `report-incomplete` + `complete-degraded` | complete a partir da mensagem, carimbe DEGRADADO e registre a falha no relatório |
112
113
  | `work-landed-report-missing` | não re-despache; grave você o relatório, DEGRADADO |
113
114
  | `no-work-no-report` / `report-missing-evidence-unknown` | re-despache uma vez; segunda ausência sem trabalho = escale |
@@ -128,10 +129,11 @@ config **antes** do verify: o stamp grava o HEAD, e código ou `config.json` com
128
129
 
129
130
  Depois `morph-spec verify {feature} --json`, **sem filtro** — nunca recorte o
130
131
  verify da feature. `--filter` é do loop do próprio sub-agente; o Gate 3 e o CI rodam a suíte
131
- inteira. Nó de teste sem `expectedCount`: acrescente `--record-baseline` a ESTA rodada — ela mede e
132
+ inteira. Nó de teste sem `expectedCount`, ou fatia que apagou teste de propósito (limpeza, `morph-spec tests-audit`): acrescente `--record-baseline` a ESTA rodada — ela mede e
132
133
  prova de uma vez; nunca um segundo verify só para gravar a baseline. Vermelho: corrija com `verify.feedback` (direto no código, ou re-despachando a fatia dona)
133
134
  e rode de novo; **3 tentativas** sem verde → `AskUserQuestion` ("Verify de {feature} vermelho após 3
134
- tentativas — aceitar com ressalva, continuar tentando, ou parar?").
135
+ tentativas — aceitar com ressalva, continuar tentando, ou parar?"). Quem conta é o `feature.json`:
136
+ `morph-spec next {feature}` devolve `fix` enquanto há tentativa e `escalate` na 3ª vermelha seguida.
135
137
 
136
138
  **Cenário que julga saída de LLM** (`prova: eval`): a evidência é rodar, não ler código. Com nó
137
139
  `runner: "evals"`: `morph-spec verify {feature} --filter {id-do-nó-evals} --skip-e2e --no-persist`
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
4
4
  version: core
5
5
  when_to_use: Use quando chegar "/morph-hotfix {feature}", quando a morph-plan rotear o pedido como workType hotfix, ou quando o usuário disser "produção caiu / hotfix / urgente" a respeito de um incidente
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-hotfix
@@ -53,8 +53,8 @@ Escreva o `plan.md` mínimo (~25 linhas) numa única escrita: o hotfix nasce com
53
53
 
54
54
  - `## Problema` — o sintoma em produção, o root cause que o scout localizou e o blast radius.
55
55
  - `## Cenários` — dois, sempre:
56
- - **C1** — o sintoma ausente: dado o estado que quebrava, quando a ação que falhava, então o resultado certo. `prova: teste` (ou `e2e`).
57
- - **C2** — a regressão do blast radius: o que o fix poderia quebrar continua funcionando. `prova: teste`.
56
+ - **C1** — o sintoma ausente: dado o estado que quebrava, quando a ação que falhava, então o resultado certo. `prova: teste` (ou `e2e`) e `risco: regressão` — o teste do bug real, o único que o hotfix escreve; o `plan-check` recusa o plano sem ele.
57
+ - **C2** — o blast radius: o que o fix poderia quebrar continua funcionando. `prova: leitura` (o caminho tocado, citado por `arquivo:linha`) ou um teste existente que já o cobre — nunca um teste novo.
58
58
  - `## Decisões` com `### Conflitos com o acervo` (`nenhum` vale).
59
59
  - `## Fatias` — uma, `### F1 — fix`, com `persona:`, `arquivos:` e `cobre: C1, C2`.
60
60
 
@@ -4,7 +4,7 @@ description: Cuida da RAM e da CPU da máquina enquanto o morph-spec trabalha
4
4
  version: core
5
5
  when_to_use: Use quando a máquina estiver lenta ou sem RAM, um comando pesado esperou ou foi recusado pela fila, sobrou worktree de sub-agent, ou vários sub-agents pesados vão rodar em paralelo
6
6
  user-invocable: false
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-machine
@@ -4,7 +4,7 @@ description: Conduz a AUTORIA de mídia gerada por IA — escolher o modelo pelo
4
4
  version: core
5
5
  when_to_use: Use quando a tarefa for PRODUZIR imagem ou vídeo por IA (catálogo, marketing, asset de LP) e alguém precisar decidir modelo, prompt e aprovação
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  stacks:
9
9
  - dotnet
10
10
  - nextjs
@@ -4,7 +4,7 @@ description: Leva uma feature do pedido ao plano aprovado — roteia o tipo de t
4
4
  version: core
5
5
  when_to_use: Use quando uma feature, bug ou chore começar ("/morph-plan {feature}", "vamos planejar X"), ou ao retomar uma feature cujo gate plan ainda não foi aprovado
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -66,19 +66,21 @@ Exit ≠ 0 → descarte o mapa e re-despache **uma** vez com as `failures` colad
66
66
 
67
67
  O `create` já escreveu o esqueleto em `2-plan/plan.md` (leia e preencha; `chore` e `hotfix` nascem com o gate carimbado e criam o arquivo numa única escrita); gramática completa: `morph-spec plan-check --help`. `plan-check` é o único leitor; o que ele não reconhece, ninguém lê.
68
68
 
69
- **Cenários** (`## Cenários`): `- **Cn** — Dado …, quando …, então …`, com `prova:` (`teste` | `e2e` | `eval` | `manual`) **na linha seguinte, sem linha em branco** — linha vazia encerra o cenário.
70
- - **Observável em teste**: que asserção eu escreveria? Sem resposta, reescreva ou descarte.
69
+ **Cenários** (`## Cenários`): `- **Cn** — Dado …, quando …, então …`, com `prova:` (`teste` | `e2e` | `eval` | `manual` | `leitura`) **na linha seguinte, sem linha em branco** — linha vazia encerra o cenário.
70
+ - **Teste é por risco, não por código novo.** `prova: teste` exige `risco:` (`regra` | `dinheiro` | `contrato` | `concorrência` | `dado` | `regressão`) na linha abaixo: ramo de regra de negócio, valor, contrato externo, corrida, integridade de dado ou o bug real. Sem risco (CRUD, DTO, mapping, wiring, constante, texto, layout) a prova é `leitura` — o verificador fecha citando `arquivo:linha` — ou `e2e` quando o fluxo é o que importa. O `plan-check` recusa `teste` sem `risco:`.
71
+ - **Observável**: que asserção (ou que linha de código) prova? Sem resposta, reescreva ou descarte.
71
72
  - **Artefato não é comportamento.** Cenário que cita `grep`, "contém a string", "o binário", "nenhum arquivo", "arquivo/pasta/diretório em" ou glob `**/` descreve o repositório. Tente o efeito em runtime; se o artefato É o entregável, justifique em `> Por que artefato:` logo abaixo (nunca na mesma linha, nunca `> [!note]`).
72
73
  - **Saída de LLM** (o que o modelo responde) prova-se por `eval` de runtime, nunca pelo texto do prompt: evidência em `3-implement/evidencias/{Cn}.md`, path listado em `arquivos:` da fatia; com nó `runner: "evals"` (standard `ai-agents-evals-with-cache`) o cenário mora no dataset. No máximo 1 teste de estrutura da fonte do prompt.
73
74
  - **"O comando de teste da raiz roda de ponta a ponta"** prova-se pelo `verify {feature}` (o nó `e2e` e o nó de teste `e2e: true` já percorrem a cadeia): nomeie essa rodada como a prova, nunca uma execução avulsa. Precisando mesmo rodar fora dele, numa linha só (o Bash não guarda o env entre chamadas): `eval "$(morph-spec env {feature})" && morph-spec heavy run -- <comando>` — o `COMPOSE_PROJECT_NAME` da feature é o que o `finish` derruba com os volumes. `prova: manual` registra passo e resultado em `3-implement/evidencias/{Cn}.md`.
74
- - **Orçamento de teste**: um teste por cenário, um de regressão por bug; acima disso, uma linha de porquê. Cenários do mesmo comportamento são linhas de tabela (`[Theory]`/`it.each`); nenhum teste que só fixa texto. O `verify` avisa quando o teste passa de 1,5× a produção.
75
+ - **Orçamento de teste**: um teste por cenário de risco — o cenário é a unidade, nunca a pós-condição nem o componente; variantes são linhas de tabela (`[Theory]`/`it.each`); acima disso, o porquê na linha do cenário. Nenhum teste que só fixa texto, prompt ou constante, nenhum de DTO/mapping, e o mesmo comportamento numa camada só (handler **ou** endpoint). Teste além do orçamento é escopo para o revisor. O `verify` avisa quando o teste passa da produção.
75
76
 
76
77
  ```markdown
77
78
  ## Cenários
78
79
  - **C1** — Dado um tenant com `criterion_overrides`, quando ele é publicado, então a publicação é recusada nomeando a chave.
79
80
  prova: teste
81
+ risco: contrato
80
82
  - **C2** — Nenhum arquivo em `tenants/**` contém a chave `criterion_overrides`.
81
- prova: teste
83
+ prova: leitura
82
84
  > Por que artefato: a chave saiu do contrato; nenhum leitor de runtime a consulta.
83
85
 
84
86
  ## Fatias
@@ -136,7 +138,7 @@ A pergunta **nomeia a feature**: é por ela que o hook `state-sync` grava a apro
136
138
 
137
139
  ## 8. Deltas por tipo
138
140
 
139
- - **`bug` (bug-lean):** a varredura roda inteira; a árvore é de repro, diagnóstico e comportamento do sistema, não de negócio; no modo automático, no máximo 1 pergunta de repro, e só se o pedido não deixar claro. `## Problema` traz repro, diagnóstico e causa provável; os cenários são o sintoma ausente e a regressão; `### Conflitos com o acervo` continua presente.
141
+ - **`bug` (bug-lean):** a varredura roda inteira; a árvore é de repro, diagnóstico e comportamento do sistema, não de negócio; no modo automático, no máximo 1 pergunta de repro, e só se o pedido não deixar claro. `## Problema` traz repro, diagnóstico e causa provável; um cenário é o sintoma ausente com `prova: teste` e `risco: regressão` (o `plan-check` recusa bug sem ele) — esse é o teste que fica; `### Conflitos com o acervo` continua presente.
140
142
  - **`chore`:** nasce com o gate `plan` carimbado por política. Escreva o plan.md curto numa única escrita (o gate já aprovado congela o arquivo depois de criado): `## Problema`, um ou dois cenários, `## Decisões` só com `### Conflitos com o acervo`, uma fatia. `plan-check` e `Skill(morph-apply)`.
141
143
  - **`hotfix`:** é da `morph-hotfix`.
142
144
 
@@ -4,7 +4,7 @@ description: Conduz o Gate 3 — despacha verificador ∥ revisor (∥ seguranç
4
4
  version: core
5
5
  when_to_use: Use quando a fase review começar, com todas as fatias com relatório final e o verify da feature verde
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -32,14 +32,15 @@ Commite o trabalho da feature (`git add <paths>` + `git commit`) e rode
32
32
  `morph-spec verify {feature} --ci --json` com `run_in_background`. Ele empurra a branch, espera o run
33
33
  e grava o stamp do artefato que o run publicou; sem CI, roda local e diz por quê.
34
34
  **Sem `--filter`, sempre**: o Gate 3 roda a suíte inteira. Esta é a única execução de testes do Gate 3 — nenhum agente
35
- roda a suíte de novo. `repeatOf` no JSON = nada mudou, use aquela rodada; `reason: "stamp-reused"` = sem CI e
36
- nada fora de `.morph/` andou desde o último verify verde: ele vale, não rode de novo.
35
+ roda a suíte de novo. `repeatOf` no JSON = nada mudou, use aquela rodada; `reason: "stamp-reused"` = nada
36
+ fora de `.morph/` andou desde o último verify verde (local ou CI): ele vale e o CI não roda de novo.
37
37
  - **Quando a saída mostrar `CI rodando`, despache (passo 3) sem esperar o fim**: o push e o
38
38
  workflow já estão commitados e a foto da árvore não os acusa. Saiu `verify local`: espere o fim
39
39
  antes do passo 3 — suíte local e `mutate` disputam a máquina. Exit 2 = trabalho fora de commit.
40
40
  - Nó `e2e`: `pass` local → `morph-spec e2e status {feature} --json` → `baseUrl` vai ao verificador.
41
41
  `fail` → correção (CI: `gh run view <id> --log-failed`). `skip` → quem classifica é o `gate-check`
42
- (`classifyE2eSkip`): dispensa de projeto passa; ambiente, `--skip-e2e` ou razão desconhecida pausam.
42
+ (`classifyE2eSkip`): dispensa de projeto e `not applicable` (feature sem UI/spec/container no diff e sem
43
+ `prova: e2e`) passam; ambiente, `--skip-e2e` ou razão desconhecida pausam.
43
44
  - `traceability.warnings` do verify vão ao revisor como sinal.
44
45
 
45
46
  ## 3. Dispatch — verificador ∥ revisor (∥ segurança)
@@ -49,7 +50,8 @@ Anote `base` (merge-base com a default) e `sha` (`git rev-parse HEAD`). Blast ra
49
50
  símbolo público do diff, `morph-spec graph affected <símbolo> --prod --json`; sem grafo
50
51
  (`no-graph`/`not-found`/`ambiguous`), siga sem o blast radius — o revisor cai para `git grep` — e
51
52
  registre a ausência no recado a ele. Cada agente nasce com contexto fresco e **nunca recebe relatório anterior** (ancoragem).
52
- Despache todos na mesma resposta:
53
+ `chore` tem review leve: só o revisor (∥ segurança, quando exigida) — sem verificador, sem cobertura de
54
+ `Cn`, sem mutação. Despache todos na mesma resposta:
53
55
  ```
54
56
  dispatch Agent (subagent_type: "morph-verifier", model: {reviewModels.verifier.model}) with prompt:
55
57
  feature, base, sha, JSON do verify (nós, e2e, URL) se já terminou, 1-design/mockups.md e
@@ -92,7 +94,8 @@ retomar, despache um novo.
92
94
  ## 5. Bloqueadores
93
95
  O avaliador também erra: **antes de editar, reverifique cada bloqueador no código** — evidência que
94
96
  não se reproduz vira pergunta ao mesmo agente, não correção. Confirmado: corrija (loop da
95
- `morph-apply`), commite, rode o `verify {feature} --ci` de novo e uma foto `--before-eval` nova.
97
+ `morph-apply`), commite, rode `verify {feature}` local (o stamp verde dele é a prova: um `--ci` depois
98
+ reusa, não roda de novo) e uma foto `--before-eval` nova.
96
99
  Retome por `SendMessage` TODO agente exigido, com o `sha` novo e `tree.dirty`/`tree.commits`: quem
97
100
  abriu bloqueadores **confirme só os bloqueadores** (retomando o MESMO agente que os abriu); os outros
98
101
  julgam só o delta `{sha anterior}..{sha novo}` — o que a correção abriu. `reviewModels.security.required`
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
4
4
  version: core
5
5
  when_to_use: Use quando precisar consultar um standard específico durante a implementação
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX de uma feature com plano aprovado
4
4
  version: core
5
5
  when_to_use: Use quando o ramo de design for tomado (morph-spec advance {feature} --design, depois do gate do plano) e antes de /morph-apply
6
6
  user-invocable: true
7
- cliVersion: "9.1.0"
7
+ cliVersion: "9.3.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
@@ -22,6 +22,7 @@ Entra depois do gate do plano, quando a `morph-plan` §6 decidiu que a feature c
22
22
 
23
23
  - WCAG AA e todos os estados de componente interativo (default/hover/focus/disabled/loading/error), não só o caminho feliz.
24
24
  - Reusa token e componente existentes antes de criar; token novo precisa de justificativa. Sistemas, não telas isoladas.
25
+ - Tarefa principal da tela em até 3 interações, uma ação primária por tela, voltar/cancelar sempre à vista. Erro diz o que fazer a seguir; conteúdo gerado por IA aparece marcado como tal.
25
26
  - **Toda tela sai com o checklist de tela preenchido.** O `advance --design` já escreveu `1-design/mockups.md` a partir do template do pacote, a fonte única dos dez itens (tabela `### Checklist de tela` dentro de cada `## Tela:`). "Não se aplica" vale se escrito; `…`, vazio ou "a definir" reprovam. Design sem tela nova ou alterada (só componente ou estilo): apague o `1-design/mockups.md` semeado. É o checklist que faz a lista de 2 000 linhas paginar: o verificador do Gate 3 cobra cada item decidido em `## Checklist de tela`. Tela de dados segue os standards de `admin-app` (lista/tabela, formulário, feedback, estrutura de página); login, onboarding, primeira tela, estado vazio de destaque e sucesso são `brand-moment`.
26
27
  - **Conformidade não basta: a régua estética vale junto.** Direção escrita antes de componente (`1-design/direcao.md`: conceito, metáfora, o que torna a tela memorável, o que ela não pode parecer; 2 ou 3 direções para tela nova ou momento de marca), celular desenhado e não derivado, movimento com versão estática digna. A régua é o standard `frontend-design-system-art-direction`.
27
28
  - **Toda tela nova ou alterada tem protótipo renderizado e conferido por print.** `1-design/prototipos/{tela}.html` autossuficiente, com os tokens reais do projeto (lidos do CSS de tokens), desktop e celular lado a lado, controles animado/estático/pausar; prints `{tela}-1280.png` e `{tela}-390.png` ao lado, saídos do laço de autocrítica. A `## Tela:` aponta o protótipo na linha `**Protótipo:**`.
@@ -966,7 +966,7 @@
966
966
  "errorbody": "#rule-2--error-path-tests-assert-the-response-body-not-just-the-status-code",
967
967
  "test-budget": "#test-budget"
968
968
  },
969
- "digest": "Inject TimeProvider (never DateTime.UtcNow / DateOnly.FromDateTime(DateTime.UtcNow) directly in a handler or validator) and fake it with FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing) in integration tests — deterministic, no midnight-UTC flake, boundary dates (today vs. yesterday) become facts about the fake clock instead of when the suite happens to run. xUnit + WebApplicationFactory error-path tests (400/404/409) must assert the ProblemDetails response BODY (title/detail/violated field or error code), never just the HTTP status code — a status-only assertion lets the wrong validation rule or the wrong error branch pass silently as long as the status matches. Test budget per task: one test per postcondition plus one regression test per bug, more only with a one-line justification in the task; scenarios of the same behavior (edge cases included) are rows of one [Theory], never copied [Fact]s; no test whose only assertion is a copy, config or prose string (that is an analyzer or a publish-time validation, outside the suite count); shared fixtures before new helpers; bugs and pure logic get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation (the Gate 3 mutation proof shows the test bites); a surviving mutant is killed by tightening an assertion or adding a row, not by a new test."
969
+ "digest": "Inject TimeProvider (never DateTime.UtcNow / DateOnly.FromDateTime(DateTime.UtcNow) directly in a handler or validator) and fake it with FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing) in integration tests — deterministic, no midnight-UTC flake, boundary dates (today vs. yesterday) become facts about the fake clock instead of when the suite happens to run. xUnit + WebApplicationFactory error-path tests (400/404/409) must assert the ProblemDetails response BODY (title/detail/violated field or error code), never just the HTTP status code — a status-only assertion lets the wrong validation rule or the wrong error branch pass silently as long as the status matches. Tests are paid for by risk, not new code: one test per plan scenario with risco: (rule, money, contract, concurrency, data, regression) plus the regression test of the bug fixed, more only with a one-line justification on the scenario; a scenario without risk (CRUD, DTO, mapping, wiring, constants) is prova: leitura and gets no test; one layer per behavior (handler or endpoint, not both); scenarios of the same behavior (edge cases included) are rows of one [Theory], never copied [Fact]s; no test whose only assertion is a copy, config or prose string (that is an analyzer or a publish-time validation, outside the suite count); shared fixtures before new helpers; bugs and risky scenarios get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation (the Gate 3 mutation proof shows the test bites); a surviving mutant is killed by tightening an assertion or adding a row, not by a new test."
970
970
  },
971
971
  {
972
972
  "id": "backend-dotnet-vsa-handler-patterns",
@@ -1516,7 +1516,7 @@
1516
1516
  "e2e": "#test-file-co-location",
1517
1517
  "test-budget": "#test-budget"
1518
1518
  },
1519
- "digest": "Vitest for unit tests, React Testing Library for component tests. Mock next/navigation with vi.mock. Test Server Actions with direct function calls. Use MSW for API mocking in component tests. Playwright for E2E tests against running app. Never test implementation details — test user behavior and output. Test budget per task: one test per postcondition plus one regression test per bug, more only with a one-line justification in the task; scenarios of the same behavior are rows of one it.each, never copied it() blocks; no test whose only assertion is a copy string (lint or publish-time validation instead); shared wrappers, MSW handlers and factories before new helpers; bugs and pure logic get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation."
1519
+ "digest": "Vitest for unit tests, React Testing Library for component tests. Mock next/navigation with vi.mock. Test Server Actions with direct function calls. Use MSW for API mocking in component tests. Playwright for E2E tests against running app. Never test implementation details — test user behavior and output. Tests are paid for by risk, not new code: one test per plan scenario with risco: plus the regression test of the bug fixed, more only with a one-line justification on the scenario; a component is not a budget unit — rendering, layout and copy are prova: leitura or e2e; scenarios of the same behavior are rows of one it.each, never copied it() blocks; no test whose only assertion is a copy string (lint or publish-time validation instead); shared wrappers, MSW handlers and factories before new helpers; bugs and risky scenarios get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation."
1520
1520
  },
1521
1521
  {
1522
1522
  "id": "frontend-scroll-driven-frame-scrub",
@@ -1123,7 +1123,7 @@
1123
1123
  "MCP server",
1124
1124
  "MCP client"
1125
1125
  ],
1126
- "verifiedAgainst": "Claude Code 2.1.282 MCP tooling — context7, Playwright (as plugin and as a project MCP server); GitHub via `gh` CLI; Neon via its MCP or `neonctl`.",
1126
+ "verifiedAgainst": "Claude Code 2.1.282 MCP tooling — context7, Playwright (as plugin and as a project MCP server); GitHub via `gh` CLI; Neon via its MCP or `neonctl`; Microsoft Learn MCP (`https://learn.microsoft.com/api/mcp`, tools listed by `tools/list` on 2026-10-17).",
1127
1127
  "provedBy": []
1128
1128
  },
1129
1129
  "frontend-admin-app-data-table": {
@@ -331,6 +331,8 @@ AIAgent instrumented = agent.AsBuilder()
331
331
 
332
332
  ## 7. What NOT to do (anti-patterns)
333
333
 
334
+ `morph-spec stack-scan` cobra três linhas desta tabela no código: Semantic Kernel, hosted agent service e versão flutuante.
335
+
334
336
  | Wrong | Right |
335
337
  |-------|-------|
336
338
  | `using Microsoft.SemanticKernel` | `using Microsoft.Agents.AI` |
@@ -115,8 +115,12 @@ respectively and the test suite stays green.
115
115
 
116
116
  ## Test budget
117
117
 
118
- The plan (`morph-plan`) gives each task a budget: **one test per postcondition, plus one
119
- regression test per bug fixed**. Going above it takes a one-line justification in the task.
118
+ Tests are paid for by **risk, not by new code**. The plan (`morph-plan`) marks each scenario
119
+ with `prova:` and, for a test, a `risco:` (business rule, money, external contract, concurrency,
120
+ data integrity, regression). The budget is **one test per risky scenario, plus the regression test of
121
+ the bug being fixed**; going above it takes a one-line justification on the scenario. A scenario
122
+ without risk — CRUD, DTO, mapping, wiring, constants, layout — is `prova: leitura`: the Gate 3
123
+ verifier proves it by reading the code, and it gets no test.
120
124
 
121
125
  - **Table before copy.** Scenarios of the same behavior — including its edge cases (null, empty,
122
126
  boundary) — are rows of one `[Theory]` (`[InlineData]`/`[MemberData]`), never N `[Fact]` methods
@@ -125,11 +129,13 @@ regression test per bug fixed**. Going above it takes a one-line justification i
125
129
  UI copy, tenant config or prose proves the file's version, not behavior. Enforce it with an
126
130
  analyzer, a lint rule or a validation at publish time, outside the suite count. The versioned
127
131
  prompt case (at most one structural test) is `ai-agents-testing-ai`.
132
+ - **One layer per behavior.** A rule tested in the handler is not tested again through the
133
+ endpoint or the SQL integration: pick the cheapest layer that bites (wiring is the verifier's).
128
134
  - **Shared fixture by default.** Reuse the project's `WebApplicationFactory` fixture, fakes and
129
135
  builders. Before writing a new helper, look for the existing one (`morph-spec graph explain
130
136
  <Name>`; fallback without a graph: `grep -rn`).
131
- - **Order is free, proof is not the implementer's:** a bug gets a regression test, and pure logic
132
- gets its tests, in whatever order the code comes out. The implementer does not run red→green
137
+ - **Order is free, proof is not the implementer's:** a bug gets a regression test, and a risky
138
+ scenario gets its test, in whatever order the code comes out. The implementer does not run red→green
133
139
  cycles nor mutate production code to watch a test fail; it runs its own filter once before the
134
140
  report, and the Gate 3 mutation proof is what shows the test bites.
135
141
  - **A surviving mutant** of the Gate 3 mutation proof is killed by tightening an existing assertion