@polymorphism-tech/morph-spec 8.24.3 → 8.26.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 (43) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +2 -1
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +2 -2
  7. package/framework/MORPH.md +22 -21
  8. package/framework/agents.json +1 -1
  9. package/framework/commands/create-umbraco-lp.md +1 -1
  10. package/framework/commands/migrate-lp-to-umbraco.md +1 -1
  11. package/framework/commands/morph-apply.md +4 -4
  12. package/framework/commands/morph-hotfix.md +3 -3
  13. package/framework/commands/morph-proposal.md +5 -5
  14. package/framework/docs/CLI.md +4 -4
  15. package/framework/evals/rubrics/condensed.md +2 -2
  16. package/framework/evals/rubrics/test-coverage.md +7 -0
  17. package/framework/hooks/claude-code/core/protect-spec-files.js +115 -4
  18. package/framework/hooks/shared/payload-utils.js +19 -4
  19. package/framework/rules/dotnet-templates.md +2 -1
  20. package/framework/rules/morph-workflow.md +42 -29
  21. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  22. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  23. package/framework/skills/core/morph-apply/SKILL.md +6 -6
  24. package/framework/skills/core/morph-brainstorming/SKILL.md +2 -2
  25. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  26. package/framework/skills/core/morph-hotfix/SKILL.md +7 -7
  27. package/framework/skills/core/morph-implement/SKILL.md +21 -6
  28. package/framework/skills/core/morph-plan/SKILL.md +15 -3
  29. package/framework/skills/core/morph-proposal/SKILL.md +7 -7
  30. package/framework/skills/core/morph-review/SKILL.md +10 -3
  31. package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
  32. package/framework/skills/core/morph-standards/SKILL.md +4 -4
  33. package/framework/skills/core/morph-uiux/SKILL.md +3 -3
  34. package/framework/templates/README.md +72 -643
  35. package/framework/templates/examples/spec-examples.md +2 -1
  36. package/package.json +1 -1
  37. package/src/commands/project/claim.js +19 -3
  38. package/src/commands/state/approve.js +17 -3
  39. package/src/commands/state/finish.js +202 -15
  40. package/src/commands/state/gate-check.js +9 -20
  41. package/src/commands/state/report-check.js +157 -5
  42. package/src/lib/evaluator-report.js +72 -0
  43. package/src/lib/risk-detector.js +35 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Leitura do `4-review/evaluator-report.md` — o relatório BRUTO do avaliador
3
+ * independente despachado pela `morph-review` no Gate 3.
4
+ *
5
+ * Existe como módulo próprio porque DOIS comandos dependem do mesmo arquivo por
6
+ * razões diferentes, e antes o liam de maneiras diferentes:
7
+ *
8
+ * - `gate-check` lê o token `**Composite-Score:**` para saber o score REAL da
9
+ * feature (em vez do proxy de médias de auto-avaliação);
10
+ * - `approve` exige o arquivo como precondição do Gate 3.
11
+ *
12
+ * A divergência era concreta: `approve.js` montava o path à mão com `join(...)`
13
+ * enquanto `gate-check.js` resolvia por `getAbsoluteOutputPath`. Dois caminhos
14
+ * para o mesmo arquivo é a receita de drift silencioso (a lição A5), e a
15
+ * checagem por `existsSync` puro do `approve` aceitava um relatório SEM o token
16
+ * que o `gate-check` então descartava — o Gate 3 passava carimbado `auto` sobre
17
+ * uma média de auto-avaliação, exatamente o que MORPH.md §5 proíbe.
18
+ */
19
+
20
+ import { existsSync, readFileSync } from 'fs';
21
+ import { getAbsoluteOutputPath } from '../core/paths/output-schema.js';
22
+
23
+ /**
24
+ * O token machine-readable que o avaliador grava por contrato (`morph-review`
25
+ * §4). Espelho de `REVIEWED_SHA_TOKEN` do approve.js — mesma ideia: um relatório
26
+ * de LLM só é auditável por máquina se carregar um campo que a máquina lê.
27
+ */
28
+ export const COMPOSITE_SCORE_TOKEN = '**Composite-Score:**';
29
+
30
+ const COMPOSITE_SCORE_RE = /\*\*Composite-Score:\*\*\s*([0-9]+(?:\.[0-9]+)?)/;
31
+
32
+ /** Path canônico do evaluator-report da feature. Fonte única para os dois comandos. */
33
+ export function evaluatorReportPath(cwd, featureName) {
34
+ return getAbsoluteOutputPath(cwd, featureName, 'evaluatorReport');
35
+ }
36
+
37
+ /**
38
+ * Score composto REAL da feature, do token gravado pelo avaliador independente.
39
+ *
40
+ * @returns {number|undefined} undefined quando o relatório não existe, não é
41
+ * legível, ou existe sem o token (relatório legado ou oco). `undefined` é
42
+ * deliberado e não 0: quem chama distingue "sem veredito" de "veredito ruim".
43
+ */
44
+ export function evaluatorCompositeScore(cwd, featureName) {
45
+ try {
46
+ const p = evaluatorReportPath(cwd, featureName);
47
+ if (!existsSync(p)) return undefined;
48
+ const m = readFileSync(p, 'utf8').match(COMPOSITE_SCORE_RE);
49
+ return m ? Number(m[1]) : undefined;
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Estado do relatório para a precondição do Gate 3.
57
+ *
58
+ * Três valores em vez de booleano PORQUE AS TRÊS SITUAÇÕES PEDEM MENSAGENS
59
+ * DIFERENTES: arquivo ausente é "despache o avaliador"; arquivo sem token é
60
+ * "o avaliador rodou mas o relatório está oco — ele não seguiu o contrato";
61
+ * e só o terceiro caso é aprovável.
62
+ *
63
+ * @returns {{ status: 'ok'|'missing'|'untokenized', score?: number, path: string }}
64
+ */
65
+ export function evaluatorReportState(cwd, featureName) {
66
+ const path = evaluatorReportPath(cwd, featureName);
67
+ if (!existsSync(path)) return { status: 'missing', path };
68
+ const score = evaluatorCompositeScore(cwd, featureName);
69
+ return score === undefined
70
+ ? { status: 'untokenized', path }
71
+ : { status: 'ok', score, path };
72
+ }
@@ -11,6 +11,8 @@
11
11
  * - verificationStatus: string — último `morph-spec verify` ('fail' pausa, qualquer gate)
12
12
  * - verificationStale + gate: bool/str — stamp com sha != HEAD pausa SÓ no
13
13
  * ship (gate 'review') — código mudou depois do último verify
14
+ * - evalScoreSource + gate: str — no ship, score que NÃO veio do avaliador
15
+ * independente pausa (auto-avaliação não decide o Gate 3)
14
16
  * - evalScore: number — morph-eval score (pause if < 9)
15
17
  * - taskCount: number — total tasks (pause if > 20)
16
18
  */
@@ -18,6 +20,13 @@
18
20
  const SCOPE_TASK_THRESHOLD = 20;
19
21
  const MIN_EVAL_SCORE = 9;
20
22
 
23
+ // A ÚNICA procedência de `evalScore` que pode decidir o gate de ship. Escrita
24
+ // por gate-check.js quando o token `**Composite-Score:**` foi lido do
25
+ // evaluator-report.md — o veredito do avaliador independente em contexto
26
+ // fresco. Toda outra procedência (proxy de taskScores, override de --signals,
27
+ // ou nenhuma) é auto-avaliação ou palavra de quem já estava no contexto.
28
+ const TRUSTED_EVAL_SOURCE = 'evaluator-report';
29
+
21
30
  // Work types whose ship gate (review) always pauses for a human, regardless
22
31
  // of trust mode — a type policy, not a per-feature risk signal. Mirrors
23
32
  // `forcedPauseGates` in src/core/work-types.js (hotfix.review); kept here too
@@ -80,6 +89,28 @@ export function shouldPauseGate(config, signals) {
80
89
  };
81
90
  }
82
91
 
92
+ // No gate de SHIP, um `evalScore` que não veio do avaliador independente NÃO
93
+ // é sinal de qualidade da feature — é sinal de qualidade das tasks, dado por
94
+ // quem as implementou. Observado em campo: 7 tasks com auto-score 9.0-9.3
95
+ // levaram o gate-check a imprimir "auto-pass, nenhum sinal de risco" sobre um
96
+ // produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1.
97
+ //
98
+ // O predicado é por AUSÊNCIA da fonte confiável, não por presença do proxy,
99
+ // porque há três maneiras de chegar aqui sem avaliador e todas devem pausar:
100
+ // proxy de taskScores; override manual via --signals; e nenhuma fonte (sem
101
+ // task pontuada e sem relatório, `evalScore` é OMITIDO e a regra de score
102
+ // abaixo nem chega a rodar).
103
+ //
104
+ // Fora do review não vale: em plan/uiux/proposal o proxy é exatamente o sinal
105
+ // certo — não existe avaliador independente antes de haver implementação.
106
+ if (signals.gate === 'review' && signals.evalScoreSource !== TRUSTED_EVAL_SOURCE) {
107
+ return {
108
+ pause: true,
109
+ reason: `Gate 3 sem score do avaliador independente (fonte: ${signals.evalScoreSource ?? 'nenhuma'})`
110
+ + ' — auto-avaliação de quem implementou não decide o ship',
111
+ };
112
+ }
113
+
83
114
  if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
84
115
  return {
85
116
  pause: true,
@@ -112,6 +143,9 @@ export function getRiskSignals(signals) {
112
143
  if (signals.gate === 'review' && signals.verificationStale === true) {
113
144
  out.push('verification stamp desatualizado (stale) no ship');
114
145
  }
146
+ if (signals.gate === 'review' && signals.evalScoreSource !== TRUSTED_EVAL_SOURCE) {
147
+ out.push(`score do Gate 3 sem avaliador independente (fonte: ${signals.evalScoreSource ?? 'nenhuma'})`);
148
+ }
115
149
  if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
116
150
  out.push(`eval score ${signals.evalScore}/10`);
117
151
  }
@@ -125,4 +159,5 @@ export const RISK_CONSTANTS = {
125
159
  SCOPE_TASK_THRESHOLD,
126
160
  MIN_EVAL_SCORE,
127
161
  FORCED_PAUSE_WORK_TYPES,
162
+ TRUSTED_EVAL_SOURCE,
128
163
  };