@polymorphism-tech/morph-spec 8.43.0 → 8.45.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 (203) hide show
  1. package/CLAUDE.md +16 -13
  2. package/README.md +10 -10
  3. package/bin/morph-spec.js +10 -13
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -4
  7. package/docs/QUICKSTART.md +6 -6
  8. package/framework/CLAUDE.md +6 -8
  9. package/framework/MORPH.md +39 -45
  10. package/framework/agents.json +8 -9
  11. package/framework/commands/morph-apply.md +1 -3
  12. package/framework/commands/morph-hotfix.md +1 -1
  13. package/framework/commands/morph-proposal.md +2 -2
  14. package/framework/docs/CLI.md +22 -17
  15. package/framework/evals/review-checklist.md +90 -0
  16. package/framework/evals/trace-schema.md +2 -12
  17. package/framework/hooks/README.md +2 -3
  18. package/framework/hooks/claude-code/core/heavy-gate.js +1 -1
  19. package/framework/hooks/claude-code/core/protect-spec-files.js +2 -2
  20. package/framework/hooks/claude-code/core/telemetry-log.js +2 -2
  21. package/framework/hooks/claude-code/core/trace-autogen.js +1 -22
  22. package/framework/hooks/claude-code/core/validate-completion.js +3 -56
  23. package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
  24. package/framework/hooks/claude-code/statusline.sh +11 -11
  25. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  26. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  27. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  28. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  29. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  30. package/framework/hooks/shared/build-check.js +1 -1
  31. package/framework/hooks/shared/comment-scanner.js +1 -1
  32. package/framework/hooks/shared/phase-utils.js +0 -5
  33. package/framework/hooks/shared/stdin-reader.js +26 -26
  34. package/framework/phases.json +4 -6
  35. package/framework/rubrics/backend.json +1 -1
  36. package/framework/rubrics/default.json +1 -1
  37. package/framework/rubrics/frontend.json +1 -1
  38. package/framework/rules/ai-agents-standards.md +1 -0
  39. package/framework/rules/morph-workflow.md +6 -6
  40. package/framework/schemas/feature.schema.json +2 -28
  41. package/framework/schemas/tasks.schema.json +6 -2
  42. package/framework/skills/README.md +1 -2
  43. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  44. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  45. package/framework/skills/core/morph-apply/SKILL.md +145 -246
  46. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  47. package/framework/skills/core/morph-hotfix/SKILL.md +5 -3
  48. package/framework/skills/core/morph-implement/SKILL.md +101 -201
  49. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  50. package/framework/skills/core/morph-media/SKILL.md +1 -1
  51. package/framework/skills/core/morph-plan/SKILL.md +171 -287
  52. package/framework/skills/core/morph-proposal/SKILL.md +4 -5
  53. package/framework/skills/core/morph-review/SKILL.md +109 -430
  54. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  55. package/framework/skills/core/morph-standards/SKILL.md +2 -2
  56. package/framework/skills/core/morph-uiux/SKILL.md +2 -3
  57. package/framework/standards/STANDARDS.json +54 -10
  58. package/framework/standards/ai-agents/agent-archetypes.md +1 -1
  59. package/framework/standards/ai-agents/agent-session.md +1 -1
  60. package/framework/standards/ai-agents/agent-spec.md +1 -1
  61. package/framework/standards/ai-agents/batching.md +1 -1
  62. package/framework/standards/ai-agents/context-providers.md +1 -1
  63. package/framework/standards/ai-agents/conversational-agent-with-phases.md +1 -1
  64. package/framework/standards/ai-agents/cost-and-budget.md +1 -1
  65. package/framework/standards/ai-agents/durable-workflows-hitl.md +1 -1
  66. package/framework/standards/ai-agents/guardrails.md +1 -1
  67. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  68. package/framework/standards/ai-agents/mcp-server.md +1 -1
  69. package/framework/standards/ai-agents/mcp-tools.md +1 -1
  70. package/framework/standards/ai-agents/media-pipeline.md +1 -1
  71. package/framework/standards/ai-agents/media-video.md +1 -1
  72. package/framework/standards/ai-agents/middleware-patterns.md +1 -1
  73. package/framework/standards/ai-agents/modalities-image-gen.md +1 -1
  74. package/framework/standards/ai-agents/modalities-speech-to-text.md +1 -1
  75. package/framework/standards/ai-agents/modalities-text-to-speech.md +1 -1
  76. package/framework/standards/ai-agents/modalities-vision.md +1 -1
  77. package/framework/standards/ai-agents/multi-agent-patterns.md +1 -1
  78. package/framework/standards/ai-agents/observability-patterns.md +3 -3
  79. package/framework/standards/ai-agents/production.md +2 -2
  80. package/framework/standards/ai-agents/prompt-sources.md +1 -1
  81. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  82. package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -1
  83. package/framework/standards/ai-agents/service-tier-flex.md +1 -1
  84. package/framework/standards/ai-agents/setup.md +1 -1
  85. package/framework/standards/ai-agents/structured-output.md +1 -1
  86. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  87. package/framework/standards/ai-agents/testing-ai.md +1 -1
  88. package/framework/standards/ai-agents/tool-adapters.md +226 -0
  89. package/framework/standards/ai-agents/vector-data-extensions.md +1 -1
  90. package/framework/standards/ai-agents/workflows.md +1 -1
  91. package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
  92. package/framework/templates/CLAUDE.md.core.template +1 -1
  93. package/framework/templates/IDE-SNIPPETS.md +262 -262
  94. package/framework/templates/README.md +1 -1
  95. package/framework/templates/REGISTRY.json +0 -81
  96. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  97. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  98. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  99. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  100. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  101. package/framework/templates/docs/spec.md +19 -205
  102. package/framework/templates/docs/ui-components.md +52 -52
  103. package/framework/templates/docs/ui-design-system.md +76 -76
  104. package/framework/templates/docs/ui-flows.md +167 -167
  105. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  106. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  107. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  108. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  109. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  110. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  111. package/framework/templates/infrastructure/github/README.md +56 -56
  112. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  113. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  114. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  115. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  116. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  117. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  118. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  119. package/framework/templates/state.template.json +17 -17
  120. package/framework/templates/ui/design-system.css +226 -226
  121. package/package.json +2 -2
  122. package/src/commands/e2e/e2e.js +1 -1
  123. package/src/commands/project/heavy.js +1 -1
  124. package/src/commands/project/status.js +3 -3
  125. package/src/commands/project/worktree.js +34 -16
  126. package/src/commands/state/approve.js +9 -50
  127. package/src/commands/state/finish.js +4 -0
  128. package/src/commands/state/gate-check.js +18 -107
  129. package/src/commands/state/gate-decision.js +1 -1
  130. package/src/commands/state/report-check.js +42 -17
  131. package/src/commands/validation/mutate.js +1 -1
  132. package/src/commands/validation/scout-check.js +28 -0
  133. package/src/commands/validation/validate-feature.js +5 -32
  134. package/src/commands/validation/verify.js +16 -25
  135. package/src/core/paths/output-schema.js +0 -2
  136. package/src/core/state/state-manager.js +1 -2
  137. package/src/core/templates/template-renderer.js +477 -477
  138. package/src/dashboard/app/lib/format.js +5 -0
  139. package/src/dashboard/app/main.jsx +13 -13
  140. package/src/dashboard/app/pages/FeatureDetail.jsx +16 -22
  141. package/src/dashboard/app/pages/Features.jsx +7 -7
  142. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  143. package/src/dashboard/app/pages/Rubrics.jsx +7 -11
  144. package/src/dashboard/scanner/docs-scanner.js +1 -2
  145. package/src/dashboard/scanner/features-scanner.js +3 -4
  146. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  147. package/src/dashboard/vite.config.js +24 -24
  148. package/src/lib/agents/output-collector.js +0 -1
  149. package/src/lib/detectors/design-system-detector.js +188 -188
  150. package/src/lib/e2e/compose-env.js +5 -2
  151. package/src/lib/evaluator-report.js +52 -334
  152. package/src/lib/generators/design-system-generator.js +508 -508
  153. package/src/lib/generators/handoff-generator.js +4 -5
  154. package/src/lib/git/evidence-staleness.js +39 -0
  155. package/src/lib/installers/memory-installer.js +2 -2
  156. package/src/lib/mutation/manifest.js +8 -2
  157. package/src/lib/mutation/report.js +5 -3
  158. package/src/lib/mutation/runner.js +9 -7
  159. package/src/lib/risk-detector.js +63 -125
  160. package/src/lib/scout/scout-check.js +293 -0
  161. package/src/lib/tasks/eval-node.js +4 -34
  162. package/src/lib/tasks/task-classifier.js +30 -30
  163. package/src/lib/tasks/task-parser.js +209 -209
  164. package/src/lib/tasks/tasks-json-parser.js +7 -5
  165. package/src/lib/tasks/test-nodes.js +1 -1
  166. package/src/lib/tasks/test-runner.js +1 -1
  167. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
  168. package/src/lib/validators/css/css-validator.js +340 -340
  169. package/src/lib/validators/reuse/reuse-validator.js +6 -8
  170. package/src/lib/validators/verify-runner.js +1 -1
  171. package/src/lib/worktree/env-allocator.js +35 -5
  172. package/src/scripts/setup-infra.js +1 -1
  173. package/src/utils/agent-id-mapper.js +11 -11
  174. package/src/utils/agents-installer.js +5 -9
  175. package/src/utils/claude-settings-manager.js +300 -300
  176. package/src/utils/hooks-installer.js +3 -10
  177. package/src/utils/skills-installer.js +1 -1
  178. package/framework/evals/proof-rubrics/README.md +0 -76
  179. package/framework/evals/proof-rubrics/eval-de-modelo.md +0 -80
  180. package/framework/evals/proof-rubrics/fiacao.md +0 -54
  181. package/framework/evals/proof-rubrics/guard-cego.md +0 -52
  182. package/framework/evals/proof-rubrics/media-claims.md +0 -109
  183. package/framework/evals/proof-rubrics/mutation-proof.md +0 -107
  184. package/framework/evals/proof-rubrics/observabilidade-genai.md +0 -119
  185. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +0 -48
  186. package/framework/evals/rubrics/README.md +0 -45
  187. package/framework/evals/rubrics/architecture.md +0 -103
  188. package/framework/evals/rubrics/code-quality.md +0 -98
  189. package/framework/evals/rubrics/condensed.md +0 -57
  190. package/framework/evals/rubrics/contracts.md +0 -67
  191. package/framework/evals/rubrics/test-coverage.md +0 -99
  192. package/framework/evals/scenarios/dotnet-crud.yaml +0 -42
  193. package/framework/evals/scenarios/nextjs-auth.yaml +0 -38
  194. package/framework/evals/suites/standard.yaml +0 -5
  195. package/framework/hooks/claude-code/core/post-edit-typecheck.js +0 -127
  196. package/framework/skills/core/morph-eval/SKILL.md +0 -64
  197. package/framework/templates/.idea/morph-templates.xml +0 -92
  198. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
  199. package/framework/templates/docs/clarifications.md +0 -253
  200. package/framework/templates/docs/onboarding.md +0 -122
  201. package/framework/templates/docs/schema-analysis.md +0 -119
  202. package/framework/templates/docs/user-stories.md +0 -34
  203. package/src/commands/state/score.js +0 -147
@@ -35,7 +35,6 @@ const POINTER_CANDIDATES = [
35
35
  // Behaviour before solution — the same order morph-plan writes them in.
36
36
  ['2-plan/usecases', 'use cases: comportamento e pós-condições testáveis (pasta)'],
37
37
  ['2-plan/spec.md', 'especificação técnica'],
38
- ['2-plan/mandate.md', 'contrato de implementação (leia antes de cada task)'],
39
38
  ['2-plan/tasks.json', 'DAG de tasks (status vivo)'],
40
39
  ['decisions.md', 'decisões registradas'],
41
40
  ['3-implement/recap.md', 'recap da implementação'],
@@ -97,18 +96,18 @@ function nextAction(feature, phase, tasks, featureName) {
97
96
  // the derived phase only flips when 2-plan/spec.md lands (derivePhase is
98
97
  // artifact-first). Pointing back at the proposal here would be wrong.
99
98
  return approved('proposal')
100
- ? `Gate 1 aprovado — produza \`2-plan/spec.md\` + \`mandate.md\` + \`tasks.json\` (a fase avança com o primeiro artefato; Gate 2 em seguida).`
99
+ ? `Gate 1 aprovado — produza \`2-plan/spec.md\` + \`tasks.json\` (a fase avança com o primeiro artefato; Gate 2 em seguida).`
101
100
  : `Complete \`0-proposal/proposal.md\` e aprove o Gate 1 (\`morph-spec advance ${featureName}\`). Skill: /morph-proposal ${featureName}.`;
102
101
  case 'uiux':
103
102
  return approved('uiux')
104
- ? `Gate de uiux aprovado — produza \`2-plan/spec.md\` + \`mandate.md\` + \`tasks.json\` (Gate 2 em seguida).`
103
+ ? `Gate de uiux aprovado — produza \`2-plan/spec.md\` + \`tasks.json\` (Gate 2 em seguida).`
105
104
  : `Complete \`1-design/design-system.md\` e aprove o gate de uiux (\`morph-spec advance ${featureName}\`).`;
106
105
  case 'plan':
107
- return `Produza \`2-plan/spec.md\` + \`mandate.md\` + \`tasks.json\` e aprove o Gate 2 (\`morph-spec advance ${featureName}\`).`;
106
+ return `Produza \`2-plan/spec.md\` + \`tasks.json\` e aprove o Gate 2 (\`morph-spec advance ${featureName}\`).`;
108
107
  case 'implement': {
109
108
  const ready = tasks?.ready?.map((t) => `${t.id} "${t.title}"`).join(', ');
110
109
  return ready
111
- ? `Execute as tasks ready — ${ready} — via /morph-apply ${featureName} (verify antes do juiz, barra 9).`
110
+ ? `Execute as tasks ready — ${ready} — via /morph-apply ${featureName} (task fecha com verify verde e critérios provados).`
112
111
  : `Todas as tasks concluídas ou bloqueadas — rode \`morph-spec advance ${featureName}\` para escancarar a review, ou destrave dependências. Skill: /morph-apply ${featureName}.`;
113
112
  }
114
113
  case 'review':
@@ -0,0 +1,39 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { getHeadSha } from './default-branch.js';
4
+
5
+ const MORPH_DIR = '.morph/';
6
+ const CONFIG_PATH = '.morph/config/config.json';
7
+
8
+ const gitOut = (cwd, args) => execFileSync('git', args, { cwd, stdio: 'pipe', maxBuffer: 64 * 1024 * 1024 }).toString();
9
+
10
+ function withoutBaseline(text) {
11
+ const config = JSON.parse(text);
12
+ if (Array.isArray(config?.project?.tests)) {
13
+ for (const node of config.project.tests) delete node?.expectedCount;
14
+ }
15
+ return config;
16
+ }
17
+
18
+ function configMoved(cwd, sha, head) {
19
+ try {
20
+ const before = withoutBaseline(gitOut(cwd, ['show', `${sha}:${CONFIG_PATH}`]));
21
+ const after = withoutBaseline(gitOut(cwd, ['show', `${head}:${CONFIG_PATH}`]));
22
+ return !isDeepStrictEqual(before, after);
23
+ } catch {
24
+ return true;
25
+ }
26
+ }
27
+
28
+ // `.morph/` is bookkeeping, except the config: it names the test commands, and only its
29
+ // expectedCount is something a baseline run writes.
30
+ export function treeMovedSince(cwd, sha, head = getHeadSha(cwd)) {
31
+ if (!sha || !head || sha === head) return false;
32
+ let changed;
33
+ try {
34
+ changed = gitOut(cwd, ['diff', '--name-only', '--no-renames', '-z', sha, head]).split('\0').filter(Boolean);
35
+ } catch {
36
+ return true;
37
+ }
38
+ return changed.some((path) => !path.startsWith(MORPH_DIR) || (path === CONFIG_PATH && configMoved(cwd, sha, head)));
39
+ }
@@ -59,7 +59,7 @@ metadata:
59
59
 
60
60
  ## Core skills
61
61
 
62
- \`morph-proposal\`, \`morph-apply\`, \`morph-brainstorming\`, \`morph-plan\`, \`morph-implement\`, \`morph-eval\`, \`morph-review\`, \`morph-stack-scan\`, \`morph-standards\`
62
+ \`morph-proposal\`, \`morph-apply\`, \`morph-brainstorming\`, \`morph-plan\`, \`morph-implement\`, \`morph-review\`, \`morph-stack-scan\`, \`morph-standards\`
63
63
 
64
64
  **Why:** This project is managed by morph-spec — spec-driven autonomous development with trust-aware gates.
65
65
 
@@ -86,7 +86,7 @@ metadata:
86
86
  - \`.morph/framework/schemas/tasks.schema.json\` — tasks.json schema reference
87
87
  - \`.morph/framework/phases.json\` — phase folder mapping and skill associations
88
88
  - \`.morph/framework/agents.json\` — domain persona definitions (dotnet-senior, ef-modeler, etc.)
89
- - \`.morph/framework/evals/rubrics/\` — eval rubrics for morph-eval scoring (architecture/contracts/code-quality/test-coverage)
89
+ - \`.morph/framework/evals/review-checklist.md\` — what the Gate 3 evaluator checks (verdict SHIP/FIX + evidence-typed blockers)
90
90
  `;
91
91
 
92
92
  /**
@@ -1,8 +1,9 @@
1
- import { isAbsolute, normalize, sep } from 'node:path';
1
+ import { posix, win32 } from 'node:path';
2
2
 
3
3
  export const MUTANT_ID = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
4
4
  export const MODES = ['schemata', 'rebuild'];
5
5
  export const BACKSLASH_TOKEN = '{{BS}}';
6
+ export const MAX_MUTANTS = 3;
6
7
 
7
8
  const expandBackslash = (s) => (typeof s === 'string' ? s.split(BACKSLASH_TOKEN).join(String.fromCharCode(92)) : s);
8
9
 
@@ -58,6 +59,9 @@ export function validateManifest(manifest, { fileExists } = {}) {
58
59
  if (!Array.isArray(list) || list.length === 0) {
59
60
  throw new ManifestError(['`mutants` precisa ser um array com ao menos um mutante']);
60
61
  }
62
+ if (list.length > MAX_MUTANTS) {
63
+ throw new ManifestError([`no máximo ${MAX_MUTANTS} mutantes por rodada: escolha as linhas de guarda ou regra que um UC exige`]);
64
+ }
61
65
  const seen = new Set();
62
66
  const mutants = list.map((raw, index) => {
63
67
  const at = `mutants[${index}]${typeof raw?.id === 'string' ? ` (${raw.id})` : ''}`;
@@ -66,17 +70,19 @@ export function validateManifest(manifest, { fileExists } = {}) {
66
70
  else if (seen.has(m.id)) problems.push(`${at}: \`id\` repetido`);
67
71
  else seen.add(m.id);
68
72
  if (typeof m.file !== 'string' || m.file === '') problems.push(`${at}: \`file\` ausente`);
69
- else if (isAbsolute(m.file) || normalize(m.file).split(sep).includes('..')) problems.push(`${at}: \`file\` tem de ser relativo à raiz do projeto, sem \`..\``);
73
+ else if (posix.isAbsolute(m.file) || win32.isAbsolute(m.file) || m.file.split(/[\\/]/).includes('..')) problems.push(`${at}: \`file\` tem de ser relativo à raiz do projeto, sem \`..\``);
70
74
  else if (fileExists && !fileExists(m.file)) problems.push(`${at}: \`file\` não existe: ${m.file}`);
71
75
  if (!Number.isInteger(m.line) || m.line < 1) problems.push(`${at}: \`line\` tem de ser inteiro >= 1`);
72
76
  if (typeof m.find !== 'string' || m.find === '') problems.push(`${at}: \`find\` ausente`);
73
77
  if (typeof m.replace !== 'string') problems.push(`${at}: \`replace\` ausente (use string vazia para apagar)`);
74
78
  else if (m.replace === m.find) problems.push(`${at}: \`replace\` igual a \`find\` não é mutação`);
75
79
  if (m.mode !== undefined && !MODES.includes(m.mode)) problems.push(`${at}: \`mode\` tem de ser ${MODES.join(' | ')}`);
80
+ if (typeof m.expect !== 'string' || m.expect.trim() === '') problems.push(`${at}: \`expect\` ausente — diga qual teste deve falhar e por quê; sem isso o mutante pode ser equivalente`);
76
81
  return {
77
82
  id: m.id, file: typeof m.file === 'string' ? m.file.replace(/\\/g, '/') : m.file,
78
83
  line: m.line, find: expandBackslash(m.find), replace: expandBackslash(m.replace), mode: m.mode ?? null,
79
84
  note: typeof m.note === 'string' ? m.note : null,
85
+ expect: typeof m.expect === 'string' ? m.expect : null,
80
86
  };
81
87
  });
82
88
  if (problems.length > 0) throw new ManifestError(problems);
@@ -13,7 +13,7 @@ function countCell(m) {
13
13
  }
14
14
 
15
15
  function reasonCell(m) {
16
- if (m.verdict === 'survived') return m.reason || 'nenhum teste falhou na suíte inteira, com o mutante compilado de verdade';
16
+ if (m.verdict === 'survived') return m.reason || `nenhum teste falhou ${m.scope === 'subset' ? 'no subconjunto' : 'na suíte inteira'}${m.expect ? ` — esperado: ${m.expect}` : ''}`;
17
17
  const how = m.demoted ? ` [rebuild: ${m.demoted}]` : '';
18
18
  return `${m.reason || '—'}${how}`;
19
19
  }
@@ -30,12 +30,14 @@ export function renderSection(result, { head = null, date = null, subset = null,
30
30
  const lines = ['## Mutações', ''];
31
31
  const { method } = result;
32
32
  const where = subset?.usable
33
- ? `${subset.classes.length} classe(s) de teste (${subset.files.changed?.length ?? 0} alterada(s) pela feature, ${subset.files.mentions?.length ?? 0} citando os tipos mutados)`
33
+ ? (subset.classes
34
+ ? `${subset.classes.length} classe(s) de teste (${subset.files?.changed?.length ?? 0} alterada(s) pela feature, ${subset.files?.mentions?.length ?? 0} citando os tipos mutados)`
35
+ : `${subset.files?.related?.length ?? 0} arquivo(s) de teste relacionados`)
34
36
  : (subset?.reason ?? 'nenhum');
35
37
  lines.push(`**Método:** \`morph-spec mutate\`${head ? ` sobre ${head.slice(0, 8)}` : ''}${date ? ` em ${date}` : ''}.`);
36
38
  lines.push(`- ${method.schemata} mutante(s) por chave em UM build (schemata), ${method.rebuild} por rebuild real${(method.jobs ?? 1) > 1 ? `; rodadas de escopo rápido em ${method.jobs} workers` : ''}.`);
37
39
  lines.push(`- Escopo rápido: ${method.subset.live ? where : `desligado (${method.subset.note ?? subset?.reason ?? 'sem subconjunto'})`}.`);
38
- lines.push('- Morto = falha nova (ausente na rodada-base do mesmo escopo) com o motivo abaixo; na suíte inteira, a falha também tem de se repetir. Sobrevivente de escopo rápido é SEMPRE refeito com o mutante compilado de verdade e a suíte inteira; "sobreviveu" só sai desse caminho.');
40
+ lines.push('- Morto = falha nova (ausente na rodada-base do mesmo escopo). Com subconjunto, a decisão fica nele: sobrevivente não é refeito na suíte inteira.');
39
41
  if (result.baseline.subset) lines.push(`- Rodada-base do subconjunto: ${result.baseline.subset.executed} executados, ${result.baseline.subset.failed} falhando.`);
40
42
  if (result.baseline.full) lines.push(`- Rodada-base da suíte inteira: ${result.baseline.full.executed} executados, ${result.baseline.full.failed} falhando.`);
41
43
  if (note) lines.push(`- ${note}`);
@@ -98,7 +98,7 @@ export async function runMutation(input) {
98
98
  };
99
99
 
100
100
  const results = new Map(regions.map((r) => [r.id, {
101
- id: r.id, file: r.file, line: r.line, diff: diffOf(r), note: r.note,
101
+ id: r.id, file: r.file, line: r.line, diff: diffOf(r), note: r.note, expect: r.expect,
102
102
  md5Before: md5Of(readFileSync(join(root, r.file))),
103
103
  md5After: md5Of(applyRegions(originals.get(r.file), [r], (x) => x.replaceText).text),
104
104
  mode: r.mode === 'rebuild' || adapter.kind !== 'dotnet' || !/\.cs$/i.test(r.file) ? 'rebuild' : 'schemata',
@@ -262,12 +262,14 @@ export async function runMutation(input) {
262
262
  const base = { scope: scopeOf, executed: result.executed, durationMs: result.durationMs };
263
263
  if (verdict.state === 'killed') {
264
264
  record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason });
265
+ } else if (scopeOf === 'subset' && verdict.state === 'survived') {
266
+ record(id, { ...base, verdict: 'survived', reason: '' });
265
267
  } else {
266
268
  record(id, { ...base, verdict: null, reason: verdict.reason ?? '' });
267
269
  routed.push({ id, needs: 'full' });
268
270
  }
269
271
  }
270
- for (const r of results.values()) if (r.mode === 'rebuild') routed.push({ id: r.id, needs: subsetLive ? 'subset-then-full' : 'full' });
272
+ for (const r of results.values()) if (r.mode === 'rebuild') routed.push({ id: r.id, needs: subsetLive ? 'subset' : 'full' });
271
273
 
272
274
  for (const { id, needs } of routed) {
273
275
  applyOnDisk(id);
@@ -280,13 +282,13 @@ export async function runMutation(input) {
280
282
  continue;
281
283
  }
282
284
  }
283
- if (needs === 'subset-then-full') {
285
+ if (needs === 'subset') {
284
286
  const sub = await run('subset', `${id}-subset`, envWithout(process.env));
285
287
  const verdict = judge(sub, baselineSubset);
286
- if (verdict.state === 'killed') {
287
- record(id, { verdict: 'killed', scope: 'subset', killers: verdict.failures.length, executed: sub.executed, reason: verdict.reason, durationMs: sub.durationMs });
288
- continue;
289
- }
288
+ const base = { scope: 'subset', executed: sub.executed, durationMs: sub.durationMs };
289
+ if (verdict.state === 'killed') record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason });
290
+ else record(id, { ...base, verdict: verdict.state, reason: verdict.reason ?? '' });
291
+ continue;
290
292
  }
291
293
  const full = await run('full', `${id}-full`, envWithout(process.env));
292
294
  const first = judge(full, baselineFull);
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Signals checked (in order):
9
9
  * - designSystemDivergence: boolean — conflicting design sources detected
10
- * - standardsViolations: string[] — violations of mandate.md proibições
10
+ * - standardsViolations: string[] — violations of the plan's forbidden patterns
11
11
  * - verificationStatus: string — último `morph-spec verify` ('fail' pausa, qualquer gate)
12
12
  * - verificationScope + gate: string — presente e != 'feature' pausa SÓ no ship: o
13
13
  * último `verify` foi um recorte de task, não da feature inteira (`null`/ausente
@@ -15,33 +15,24 @@
15
15
  * - e2eStatus + e2eReason + gate — nó e2e do último verify. Um `skip`
16
16
  * cuja razão NÃO seja dispensa legítima pausa SÓ no ship (gate 'review').
17
17
  * Sem escopo (stamp sem o campo) cai do mesmo lado seguro que `unknown`
18
- * - verificationStale + gate: bool/str — stamp com sha != HEAD pausa SÓ no
19
- * ship (gate 'review') — código mudou depois do último verify
20
- * - evalScoreSource + gate: str — no ship, score que NÃO veio do avaliador
21
- * independente pausa (auto-avaliação não decide o Gate 3)
22
- * - mutationProof + gate: object — { section, min, productionCode } lido do
23
- * evaluator-report.md. No ship, relatório SEM a seção `## Mutações` numa
24
- * feature com código de produção pausa: score é opinião, mutação é prova
25
- * - mediaClaims + gate: object — { section, score, featureGeneratesMedia }
18
+ * - verificationStale + gate: bool/str — stamp de um commit cujo código já mudou
19
+ * (fora de .morph/) pausa SÓ no ship (gate 'review')
20
+ * - evaluatorVerdict + gate: object — { status, verdict, blockers, untyped } do
21
+ * evaluator-report.md. No ship, só `SHIP` sem bloqueador libera
22
+ * - mediaClaims + gate: object — { section, featureGeneratesMedia }
26
23
  * - screenChecklist + gate: object — { mockups, legacy, screens, problems, exempt?,
27
24
  * section?, uncovered? } derivado do 1-design/mockups.md. No uiux, checklist
28
25
  * de tela incompleto pausa; no ship, item decidido sem a conferência do
29
26
  * avaliador pausa. Mockup legado é isento nos dois, com a razão listada
30
27
  * - oversizedTasks + gate: array — tasks acima de MAX_TASK_OUTPUTS outputs;
31
28
  * pausa SÓ no plano (gate 'plan'): a task tem de caber num dispatch
32
- * - evalScore: number — morph-eval score (pause if < 9)
33
29
  * - taskCount: number — total tasks (pause if > 20)
34
30
  */
35
31
 
36
32
  const SCOPE_TASK_THRESHOLD = 20;
37
- const MIN_EVAL_SCORE = 9;
38
33
 
39
- // A ÚNICA procedência de `evalScore` que pode decidir o gate de ship. Escrita
40
- // por gate-check.js quando o token `**Composite-Score:**` foi lido do
41
- // evaluator-report.md — o veredito do avaliador independente em contexto
42
- // fresco. Toda outra procedência (proxy de taskScores, override de --signals,
43
- // ou nenhuma) é auto-avaliação ou palavra de quem já estava no contexto.
44
- const TRUSTED_EVAL_SOURCE = 'evaluator-report';
34
+ // Caminho alterado que pede o avaliador em opus: o resto da review roda em sonnet.
35
+ const OPUS_PATH_RE = /(^|[/._-])(auth|login|senha|password|token|secret|crypto|permiss|rbac|acl|lgpd|pii|privacy|payment|pagamento|billing|cobranca|invoice|asaas|stripe|migrations?)([/._-]|$)/i;
45
36
 
46
37
  // Work types whose ship gate (review) always pauses for a human, regardless
47
38
  // of trust mode — a type policy, not a per-feature risk signal. Mirrors
@@ -168,6 +159,50 @@ export function uiuxChecklistPause(screenChecklist) {
168
159
  + ' ("não se aplica" vale, se escrito)';
169
160
  }
170
161
 
162
+ function blockerList(v) {
163
+ return firstOf(v.blockerIds ?? []);
164
+ }
165
+
166
+ /** Por que o veredito do avaliador segura o ship, ou null. */
167
+ export function verdictPause(v) {
168
+ if (!v || v.status === 'missing') {
169
+ return 'Gate 3 sem `4-review/evaluator-report.md` — despache o avaliador independente;'
170
+ + ' auto-avaliação de quem implementou não decide o ship';
171
+ }
172
+ if (v.status !== 'ok') {
173
+ return 'evaluator-report sem `**Verdict:** SHIP|FIX` — o avaliador não seguiu o contrato do relatório';
174
+ }
175
+ if ((v.untyped ?? 0) > 0) {
176
+ return `${v.untyped} bloqueador(es) sem tipo de evidência (uc | teste | regressao | seguranca)`
177
+ + ' — sem evidência tipada não é bloqueador: reclassifique como nota ou complete a evidência';
178
+ }
179
+ if ((v.blockers ?? 0) > 0) return `avaliador abriu ${v.blockers} bloqueador(es): ${blockerList(v)}`;
180
+ if (v.verdict !== 'SHIP') return 'veredito do avaliador é FIX';
181
+ return null;
182
+ }
183
+
184
+ function verdictSummary(v) {
185
+ if (!v || v.status !== 'ok') return 'Gate 3 sem veredito do avaliador independente';
186
+ return `veredito ${v.verdict}, ${v.blockers ?? 0} bloqueador(es)`;
187
+ }
188
+
189
+ /**
190
+ * Modelo do avaliador do Gate 3: `opus` quando a feature toca caminho sensível
191
+ * (auth, segredo, permissão, dado pessoal, dinheiro, migration), prompt/modelo
192
+ * ou geração de mídia; `sonnet` no resto.
193
+ *
194
+ * @param {{ changedFiles?: string[], promptModelTouched?: boolean, generatesMedia?: boolean }} input
195
+ * @returns {{ model: 'opus'|'sonnet', reasons: string[] }}
196
+ */
197
+ export function evaluatorModelFor({ changedFiles = [], promptModelTouched = false, generatesMedia = false } = {}) {
198
+ const reasons = [];
199
+ const risky = changedFiles.filter((f) => OPUS_PATH_RE.test(f.replaceAll('\\', '/')));
200
+ if (risky.length) reasons.push(`caminho sensível: ${firstOf(risky)}`);
201
+ if (promptModelTouched) reasons.push('prompt/modelo alterado');
202
+ if (generatesMedia) reasons.push('gera mídia');
203
+ return { model: reasons.length ? 'opus' : 'sonnet', reasons };
204
+ }
205
+
171
206
  /**
172
207
  * @param {object} config - .morph/config/config.json contents (uses `trust`)
173
208
  * @param {object} signals
@@ -274,59 +309,13 @@ export function shouldPauseGate(config, signals) {
274
309
  if (signals.gate === 'review' && signals.verificationStale === true) {
275
310
  return {
276
311
  pause: true,
277
- reason: 'verification stamp desatualizado (sha difere do HEAD) — rode `morph-spec verify` de novo antes do ship',
312
+ reason: 'verification stamp desatualizado (o código mudou depois do commit do stamp) — rode `morph-spec verify` de novo antes do ship',
278
313
  };
279
314
  }
280
315
 
281
- // No gate de SHIP, um `evalScore` que não veio do avaliador independente NÃO
282
- // é sinal de qualidade da feature — é sinal de qualidade das tasks, dado por
283
- // quem as implementou. Observado em campo: 7 tasks com auto-score 9.0-9.3
284
- // levaram o gate-check a imprimir "auto-pass, nenhum sinal de risco" sobre um
285
- // produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1.
286
- //
287
- // O predicado é por AUSÊNCIA da fonte confiável, não por presença do proxy,
288
- // porque há três maneiras de chegar aqui sem avaliador e todas devem pausar:
289
- // proxy de taskScores; override manual via --signals; e nenhuma fonte (sem
290
- // task pontuada e sem relatório, `evalScore` é OMITIDO e a regra de score
291
- // abaixo nem chega a rodar).
292
- //
293
- // Fora do review não vale: em plan/uiux/proposal o proxy é exatamente o sinal
294
- // certo — não existe avaliador independente antes de haver implementação.
295
- if (signals.gate === 'review' && signals.evalScoreSource !== TRUSTED_EVAL_SOURCE) {
296
- return {
297
- pause: true,
298
- reason: `Gate 3 sem score do avaliador independente (fonte: ${signals.evalScoreSource ?? 'nenhuma'})`
299
- + ' — auto-avaliação de quem implementou não decide o ship',
300
- };
301
- }
302
-
303
- // No gate de SHIP: o relatório do avaliador chegou SEM a seção `## Mutações`.
304
- // O score diz o que o avaliador ACHOU; a seção diz o que ele PROVOU — e um
305
- // teste verde só prova que o código roda, nunca que o teste morde. Medido:
306
- // 11 testes verdes sobre um artefato que o build recusava, porque nenhum
307
- // deles CARREGAVA o arquivo; e um caminho de escrita reduzido a `return 0;`
308
- // que passou 23 de 23, todos de recusa.
309
- //
310
- // DEPOIS da cláusula de procedência e ANTES da de score, e a ordem é
311
- // POLÍTICA, não acaso: quando faltam avaliador E seção, a razão reportada tem
312
- // de ser a mais fundamental ("sem avaliador independente"), porque exigir a
313
- // prova de um relatório que nem devia decidir o gate manda o humano consertar
314
- // a coisa errada.
315
- //
316
- // FAIL-OPEN POR AUSÊNCIA, em duas direções: `productionCode === true` é
317
- // exigido EXPLICITAMENTE, então uma feature legada (sinal ausente, `undefined`)
318
- // não pausa, e uma feature 100% `docs` — que não tem o que mutar — também
319
- // não. Quem decide as duas coisas é gate-check.js, lendo o disco; aqui só se
320
- // aplica a política sobre fatos já reunidos.
321
- if (signals.gate === 'review'
322
- && signals.mutationProof?.productionCode === true
323
- && signals.mutationProof?.section !== true) {
324
- return {
325
- pause: true,
326
- reason: 'relatório do avaliador sem a seção `## Mutações` — prova de mutação não registrada'
327
- + ' (rubrica evals/proof-rubrics/mutation-proof.md): um teste verde prova que o código'
328
- + ' roda, não que o teste morde',
329
- };
316
+ if (signals.gate === 'review') {
317
+ const reason = verdictPause(signals.evaluatorVerdict);
318
+ if (reason) return { pause: true, reason };
330
319
  }
331
320
 
332
321
  // No gate de SHIP apenas: a feature mexeu num caminho que o PROJETO declarou
@@ -368,24 +357,7 @@ export function shouldPauseGate(config, signals) {
368
357
  // também já pausou, por `verificationStale`.
369
358
  }
370
359
 
371
- // No gate de SHIP: uma feature que GERA MÍDIA cujo relatório chegou sem a
372
- // seção `## Mídia gerada`. As quatro afirmações que essa seção carrega —
373
- // custo, revisão humana, marcação e hot path não bloqueado — são fáceis de
374
- // escrever e difíceis de provar, e a mais cara delas é a que ninguém percebe
375
- // falhando: mídia gerada dentro de um turno de conversa, com latência
376
- // documentada de até 2 min, custo por chamada e ZERO revisão possível.
377
- //
378
- // DEPOIS da cláusula de procedência e da de mutação, e a ordem é POLÍTICA,
379
- // como lá: quando faltam avaliador, seção de mutação E seção de mídia, a razão
380
- // reportada tem de ser a mais fundamental, porque mandar o humano consertar a
381
- // terceira enquanto a primeira está aberta manda consertar a coisa errada.
382
- //
383
- // FAIL-OPEN POR AUSÊNCIA, igual: `featureGeneratesMedia === true` é exigido
384
- // EXPLICITAMENTE, então uma feature legada (sinal ausente, `undefined`) não
385
- // pausa, e a maioria das features — que não gera mídia — fica isenta
386
- // automaticamente, sem ninguém precisar lembrar de aplicar a exceção. Quem
387
- // decide isso é gate-check.js, lendo `standards[]` das tasks; aqui só se
388
- // aplica a política sobre fatos já reunidos.
360
+ // Feature que gera mídia: custo, revisão humana, marcação e hot path só valem provados na seção.
389
361
  if (signals.gate === 'review'
390
362
  && signals.mediaClaims?.featureGeneratesMedia === true
391
363
  && signals.mediaClaims.section !== true) {
@@ -393,7 +365,7 @@ export function shouldPauseGate(config, signals) {
393
365
  pause: true,
394
366
  reason: 'relatório do avaliador sem a seção `## Mídia gerada` — as quatro afirmações de mídia'
395
367
  + ' (custo, revisão humana, marcação, hot path) não foram provadas'
396
- + ' (rubrica evals/proof-rubrics/media-claims.md)',
368
+ + ' (evals/review-checklist.md, "Feature que gera mídia")',
397
369
  };
398
370
  }
399
371
 
@@ -417,7 +389,7 @@ export function shouldPauseGate(config, signals) {
417
389
  pause: true,
418
390
  reason: `relatório do avaliador sem a seção \`## Checklist de tela\` — o design decidiu ${firstOf(uncovered)}`
419
391
  + ' e a avaliação não registrou a conferência contra a tela entregue'
420
- + ' (rubrica evals/rubrics/contracts.md, "Contrato de tela")',
392
+ + ' (evals/review-checklist.md, "Contrato de tela")',
421
393
  };
422
394
  }
423
395
  return {
@@ -435,13 +407,6 @@ export function shouldPauseGate(config, signals) {
435
407
  };
436
408
  }
437
409
 
438
- if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
439
- return {
440
- pause: true,
441
- reason: `eval score ${signals.evalScore} below threshold ${MIN_EVAL_SCORE}`,
442
- };
443
- }
444
-
445
410
  if (typeof signals.taskCount === 'number' && signals.taskCount > SCOPE_TASK_THRESHOLD) {
446
411
  return {
447
412
  pause: true,
@@ -479,24 +444,7 @@ export function getRiskSignals(signals) {
479
444
  if (signals.gate === 'review' && signals.verificationStale === true) {
480
445
  out.push('verification stamp desatualizado (stale) no ship');
481
446
  }
482
- if (signals.gate === 'review' && signals.evalScoreSource !== TRUSTED_EVAL_SOURCE) {
483
- out.push(`score do Gate 3 sem avaliador independente (fonte: ${signals.evalScoreSource ?? 'nenhuma'})`);
484
- }
485
- // Listado mesmo quando NÃO pausa (feature só de docs, ou prova presente com
486
- // score baixo): a prova é o insumo do teto de `test-coverage.md`, e um teto
487
- // que ninguém enxerga não é aplicado por ninguém.
488
- if (signals.gate === 'review' && signals.mutationProof) {
489
- const { section, min, productionCode } = signals.mutationProof;
490
- if (section !== true) {
491
- out.push(productionCode === true
492
- ? 'relatório sem a seção `## Mutações` (prova de mutação ausente)'
493
- : 'relatório sem a seção `## Mutações` — feature sem código de produção, não pausa');
494
- } else if (typeof min === 'number') {
495
- out.push(`prova de mutação registrada (menor Proof-Score: ${min})`);
496
- } else {
497
- out.push('prova de mutação registrada (sem `**Proof-Scores:**` — nenhum teto a aplicar)');
498
- }
499
- }
447
+ if (signals.gate === 'review') out.push(verdictSummary(signals.evaluatorVerdict));
500
448
  // Listado mesmo quando NÃO pausa, pela mesma razão do e2e: um projeto que não
501
449
  // declara caminho de prompt/modelo é indistinguível, no relatório, de um que
502
450
  // declara e não tocou em nada — e as duas coisas significam coisas diferentes.
@@ -511,9 +459,6 @@ export function getRiskSignals(signals) {
511
459
  if (signals.gate === 'plan' && Array.isArray(signals.oversizedTasks) && signals.oversizedTasks.length > 0) {
512
460
  out.push(`tasks grandes demais: ${signals.oversizedTasks.map((t) => t.id).join(', ')}`);
513
461
  }
514
- if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
515
- out.push(`eval score ${signals.evalScore}/10`);
516
- }
517
462
  if (typeof signals.taskCount === 'number' && signals.taskCount > SCOPE_TASK_THRESHOLD) {
518
463
  out.push(`${signals.taskCount} tasks (over threshold)`);
519
464
  }
@@ -521,14 +466,9 @@ export function getRiskSignals(signals) {
521
466
  // de uma feature que não gera mídia tem de ser VISÍVEL, senão é
522
467
  // indistinguível de uma omissão.
523
468
  if (signals.gate === 'review' && signals.mediaClaims?.featureGeneratesMedia === true) {
524
- const { section, score } = signals.mediaClaims;
525
- if (section !== true) {
526
- out.push('relatório sem a seção `## Mídia gerada` (prova de mídia ausente)');
527
- } else if (typeof score === 'number') {
528
- out.push(`prova de mídia registrada (media=${score}; teto em arquitetura)`);
529
- } else {
530
- out.push('prova de mídia registrada (sem `media=` nos Proof-Scores — nenhum teto a aplicar)');
531
- }
469
+ out.push(signals.mediaClaims.section === true
470
+ ? 'prova de mídia registrada'
471
+ : 'relatório sem a seção `## Mídia gerada` (prova de mídia ausente)');
532
472
  }
533
473
 
534
474
  // Listado mesmo quando NÃO pausa: a isenção (sem mockup, legado, nada
@@ -563,8 +503,6 @@ export function getRiskSignals(signals) {
563
503
 
564
504
  export const RISK_CONSTANTS = {
565
505
  SCOPE_TASK_THRESHOLD,
566
- MIN_EVAL_SCORE,
567
506
  FORCED_PAUSE_WORK_TYPES,
568
- TRUSTED_EVAL_SOURCE,
569
507
  E2E_SKIP_CLASSES_THAT_PAUSE,
570
508
  };