@polymorphism-tech/morph-spec 10.8.0 → 10.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/docs/CLI.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/lib/risk/diff-risk.js +29 -8
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
|
|
4
4
|
|
|
5
5
|
**Package:** `@polymorphism-tech/morph-spec`
|
|
6
|
-
**Version:** 10.8.
|
|
6
|
+
**Version:** 10.8.1
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -376,4 +376,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
376
376
|
|
|
377
377
|
---
|
|
378
378
|
|
|
379
|
-
*morph-spec v10.8.
|
|
379
|
+
*morph-spec v10.8.1 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/MORPH.md
CHANGED
|
@@ -49,7 +49,7 @@ Na abertura do plano vale o contrário: confronte o pedido com o código e pergu
|
|
|
49
49
|
| `chore` | plano curto (congela no 1º `plan-check` verde) → implement | Gate 3 |
|
|
50
50
|
| `hotfix` | plano curto carimbado → fix → `verifier.md` seu → humano | Gate 3, sempre humano |
|
|
51
51
|
|
|
52
|
-
**A cerimônia segue o risco do diff, não o tipo** (`report-check --before-eval` → `reviewModels.risk`). Baixo (doc, `.morph/`, config, remoção, import): sem worktree, sem sub-agent (você escreve o `reviewer.md` pelo checklist), `verify --affected` é a prova. Lógica em código ou teste: verificador ∥ revisor. PII, permissão, envio a terceiro, pagamento ou contrato: + segurança.
|
|
52
|
+
**A cerimônia segue o risco do diff, não o tipo** (`report-check --before-eval` → `reviewModels.risk`). Baixo (doc, `.morph/`, config, remoção, import; prompt de agente nunca é doc): sem worktree, sem sub-agent (você escreve o `reviewer.md` pelo checklist), `verify --affected` é a prova. Lógica em código ou teste: verificador ∥ revisor. PII, permissão, envio a terceiro, pagamento ou contrato: + segurança.
|
|
53
53
|
|
|
54
54
|
Toda feature nasce em `morph/{f}` e fecha com `morph-spec finish <f> --pr|--merge`.
|
|
55
55
|
|
package/framework/docs/CLI.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
No **review**, o `gate-check` lê os relatórios de `4-review/` (`src/lib/review-reports.js` — leitor único) e grava `signals.reviewReports = {required, reports: {verifier, reviewer, security}, checks}`: exigidos são `verifier` e `reviewer`, mais `security` quando o diff toca caminho sensível; no `hotfix`, só `verifier`. Pausa o Gate 3: relatório exigido ausente, PARCIAL ou sem `**Verdict:**`; `FIX`; bloqueador aberto; bloqueador sem tipo (`check`/`teste`/`regressao`/`seguranca`/`escopo`/`arquitetura`/`performance`/`irreversivel`; um `irreversivel` aberto vai direto ao humano); e a cobertura — todo cenário do `checks.json` precisa de uma linha `PROVADO` na `## Checks` do `verifier.md` — somadas todas as seções, vale a última linha de cada Cn (`checks.missing` = sem linha, `checks.notProven` = `NÃO PROVADO`/`FALHA`), e `checks.json` ilegível também pausa. Cada relatório é append-only e vale o ÚLTIMO `**Verdict:**`. `reviewReports` não é sobrescrevível por `--signals`. Fora do review a regra não vale.
|
|
38
38
|
|
|
39
|
-
**Papéis do Gate 3 pelo risco do diff** (`src/lib/risk/diff-risk.js`, do merge-base com `origin/<default>`): `low` (só doc, `.morph/`, config, remoção ou import) exige só o `reviewer.md`, escrito pela sessão com o checklist; `normal` (lógica em código ou teste, migration, arquivo que alcança o projeto inteiro) exige verificador e revisor, inclusive num chore; PII, permissão, envio a terceiro, pagamento ou contrato, pelo caminho (camelCase também: `PiiPatterns.cs`) ou por linha adicionada, acrescentam a segurança. Hotfix segue humano. O `--json` traz `risk`. A revisão velha tem um oráculo só (`reviewConfirmation`), lido pelo `approve` e pelo `report-check`: delta trivial (`.morph/`, doc, config, import, comentário) não invalida relatório; um bloqueador do relatório relistado como **Fechado** na confirmação não bloqueia.
|
|
39
|
+
**Papéis do Gate 3 pelo risco do diff** (`src/lib/risk/diff-risk.js`, do merge-base com `origin/<default>`): `low` (só doc, `.morph/`, config, remoção ou import; prompt de agente — `prompts/`, `agents/`, `skills/`, `playbooks/`, `.claude/`, `CLAUDE.md`/`AGENTS.md`/`SKILL.md`, `*prompt*` e os globs de `project.evals.watch` — é código, e qualquer linha dele é lógica) exige só o `reviewer.md`, escrito pela sessão com o checklist; `normal` (lógica em código ou teste, migration, arquivo que alcança o projeto inteiro) exige verificador e revisor, inclusive num chore; PII, permissão, envio a terceiro, pagamento ou contrato, pelo caminho (camelCase também: `PiiPatterns.cs`) ou por linha adicionada, acrescentam a segurança. Hotfix segue humano. O `--json` traz `risk`. A revisão velha tem um oráculo só (`reviewConfirmation`), lido pelo `approve` e pelo `report-check`: delta trivial (`.morph/`, doc, config, import, comentário) não invalida relatório; um bloqueador do relatório relistado como **Fechado** na confirmação não bloqueia.
|
|
40
40
|
|
|
41
41
|
Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (o código mudou depois do commit do stamp — só conta o que está fora de `.morph/`; o `config.json` conta exceto a troca de `expectedCount`, e um commit só de estado, como o do próprio `feature.json` do verify, não invalida) força pausa **só no ship** (`review`); em `plan`/`uiux` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
|
|
42
42
|
|
|
@@ -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: "10.8.
|
|
7
|
+
cliVersion: "10.8.1"
|
|
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: "10.8.
|
|
7
|
+
cliVersion: "10.8.1"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Implementa o plano aprovado de uma feature morph-spec inteiro nesta
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use com o gate do plano aprovado ("/morph-apply {feature}", "implementar feature"), em "/morph-review {feature}" (entra no §5), em "/morph-hotfix {feature}" ou incidente de produção (references/hotfix.md), ou quando `morph-spec next` mandar implement, verify, fix, review ou finish
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.8.
|
|
7
|
+
cliVersion: "10.8.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -4,7 +4,7 @@ description: Leva um pedido ou o link de uma issue ao plano aprovado — roteia
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando uma feature, bug ou chore começar ("/morph-plan {feature}", "/morph-plan <link da issue>", "vamos planejar X"), ou ao retomar uma feature cujo gate plan ainda não foi aprovado
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.8.
|
|
7
|
+
cliVersion: "10.8.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase de UI/UX de uma feature com plano aprovado — render
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando o ramo de design for tomado (morph-spec advance {feature} --design), antes de /morph-apply
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.8.
|
|
7
|
+
cliVersion: "10.8.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-uiux
|
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'child_process';
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import { readMorphConfig } from '../tasks/test-runner.js';
|
|
2
4
|
import { isTestPath } from '../graph/sanitize.js';
|
|
3
5
|
import { contractsTouched, migrationsTouched, securityReviewFor } from '../risk-detector.js';
|
|
4
6
|
|
|
@@ -16,8 +18,16 @@ function git(cwd, args) {
|
|
|
16
18
|
return execFileSync('git', args, { cwd, stdio: 'pipe', maxBuffer: 256 * 1024 * 1024 }).toString();
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
// An agent reads these as instructions: a changed sentence is changed behaviour, whatever the extension.
|
|
22
|
+
const PROMPT_PATH = /(^|\/)(prompts?|agents?|skills?|playbooks?|personas?|instructions?|\.claude)\/|(^|\/)(CLAUDE|AGENTS|GEMINI|MORPH|SKILL|copilot-instructions)\.md$|prompt[^/]*$/i;
|
|
23
|
+
|
|
24
|
+
export function isPromptPath(path, promptGlobs = []) {
|
|
25
|
+
return PROMPT_PATH.test(path) || promptGlobs.some((g) => minimatch(path, g));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fileClass(path, promptGlobs = []) {
|
|
20
29
|
if (path.startsWith('.morph/')) return 'state';
|
|
30
|
+
if (isPromptPath(path, promptGlobs)) return 'code';
|
|
21
31
|
if (DOC_PATH.test(path)) return 'doc';
|
|
22
32
|
if (CONFIG_PATH.test(path) && !WIDE_PATH.test(path)) return 'config';
|
|
23
33
|
return isTestPath(path) || /(^|\/)e2e\//.test(path) ? 'test' : 'code';
|
|
@@ -54,6 +64,8 @@ export function diffLines(cwd, from, to = null) {
|
|
|
54
64
|
|
|
55
65
|
/** `trivial` = only imports, comments, blank lines; `removal` = logic only taken out; `logic` = logic added. */
|
|
56
66
|
export function changeKind(file) {
|
|
67
|
+
// A prompt has no comments: a heading or a bullet is an instruction.
|
|
68
|
+
if (file.prompt) return [...file.added, ...file.removed].some((l) => l.trim()) || file.deleted ? 'logic' : 'trivial';
|
|
57
69
|
if (file.deleted) return 'removal';
|
|
58
70
|
if (file.untracked) return 'logic';
|
|
59
71
|
if (file.added.some((l) => !TRIVIAL_LINE.test(l))) return 'logic';
|
|
@@ -61,10 +73,13 @@ export function changeKind(file) {
|
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
/** `low`: docs, `.morph/`, config, removals, imports. `normal`: logic in code or tests. `security` rides on either. */
|
|
64
|
-
export function classifyFiles(files) {
|
|
76
|
+
export function classifyFiles(files, { promptGlobs = [] } = {}) {
|
|
65
77
|
const reasons = [];
|
|
66
|
-
const relevant = files
|
|
67
|
-
|
|
78
|
+
const relevant = files
|
|
79
|
+
.filter((f) => fileClass(f.path, promptGlobs) !== 'state')
|
|
80
|
+
.map((f) => ({ ...f, prompt: isPromptPath(f.path, promptGlobs) }));
|
|
81
|
+
const fileClassOf = (f) => (f.prompt ? 'code' : fileClass(f.path));
|
|
82
|
+
const logic = relevant.filter((f) => ['code', 'test'].includes(fileClassOf(f)) && changeKind(f) === 'logic');
|
|
68
83
|
const wide = relevant.filter((f) => WIDE_PATH.test(f.path));
|
|
69
84
|
const paths = relevant.map((f) => f.path);
|
|
70
85
|
if (logic.length) reasons.push(`lógica em ${logic.slice(0, 3).map((f) => f.path).join(', ')}${logic.length > 3 ? ` (+${logic.length - 3})` : ''}`);
|
|
@@ -75,17 +90,23 @@ export function classifyFiles(files) {
|
|
|
75
90
|
|
|
76
91
|
const securityReasons = [...securityReviewFor(paths.map(camelSplit)).reasons];
|
|
77
92
|
if (contract.length) securityReasons.push(`contrato: ${contract[0]}`);
|
|
78
|
-
const sensitive = relevant.find((f) =>
|
|
93
|
+
const sensitive = relevant.find((f) => fileClassOf(f) === 'code' && f.added.some((l) => SENSITIVE_LINE.test(l)));
|
|
79
94
|
if (sensitive) {
|
|
80
95
|
const line = sensitive.added.find((l) => SENSITIVE_LINE.test(l)).trim();
|
|
81
96
|
securityReasons.push(`linha sensível em ${sensitive.path}: ${line.slice(0, 80)}`);
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
const level = relevant.length === 0 ? 'none' : reasons.length || securityReasons.length ? 'normal' : 'low';
|
|
85
|
-
const trivial = level !== 'normal' && relevant.every((f) => !['code', 'test'].includes(
|
|
100
|
+
const trivial = level !== 'normal' && relevant.every((f) => !['code', 'test'].includes(fileClassOf(f)) || changeKind(f) === 'trivial');
|
|
86
101
|
return { level, trivial, reasons, security: { required: securityReasons.length > 0, reasons: securityReasons } };
|
|
87
102
|
}
|
|
88
103
|
|
|
104
|
+
// The paths the project declared for its evals are its prompts and models.
|
|
105
|
+
function promptGlobsOf(cwd) {
|
|
106
|
+
const watch = readMorphConfig(cwd)?.project?.evals?.watch;
|
|
107
|
+
return Array.isArray(watch) ? watch.filter((g) => typeof g === 'string' && g.trim()) : [];
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
function mergeBase(cwd, defaultBranch) {
|
|
90
111
|
for (const ref of [`origin/${defaultBranch}`, defaultBranch]) {
|
|
91
112
|
try {
|
|
@@ -100,7 +121,7 @@ export function featureDiffRisk(cwd, defaultBranch) {
|
|
|
100
121
|
const base = defaultBranch ? mergeBase(cwd, defaultBranch) : null;
|
|
101
122
|
if (!base) return null;
|
|
102
123
|
try {
|
|
103
|
-
return { base, ...classifyFiles(diffLines(cwd, base)) };
|
|
124
|
+
return { base, ...classifyFiles(diffLines(cwd, base), { promptGlobs: promptGlobsOf(cwd) }) };
|
|
104
125
|
} catch {
|
|
105
126
|
return null;
|
|
106
127
|
}
|
|
@@ -109,7 +130,7 @@ export function featureDiffRisk(cwd, defaultBranch) {
|
|
|
109
130
|
/** The delta between two commits; `trivial` (no logic added or removed) never asks for a second review. */
|
|
110
131
|
export function deltaRisk(cwd, from, to) {
|
|
111
132
|
try {
|
|
112
|
-
return classifyFiles(diffLines(cwd, from, to));
|
|
133
|
+
return classifyFiles(diffLines(cwd, from, to), { promptGlobs: promptGlobsOf(cwd) });
|
|
113
134
|
} catch {
|
|
114
135
|
return null;
|
|
115
136
|
}
|