@polymorphism-tech/morph-spec 9.7.0 → 9.8.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 (54) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +1 -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/agents.json +1 -1
  8. package/framework/docs/CLI.md +18 -9
  9. package/framework/evals/review-checklist.md +3 -3
  10. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  11. package/framework/hooks/claude-code/statusline.sh +11 -11
  12. package/framework/hooks/shared/heavy-slots.js +41 -4
  13. package/framework/schemas/config.schema.json +1 -1
  14. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  15. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  16. package/framework/skills/core/morph-apply/SKILL.md +2 -2
  17. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  18. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  19. package/framework/skills/core/morph-media/SKILL.md +1 -1
  20. package/framework/skills/core/morph-plan/SKILL.md +2 -2
  21. package/framework/skills/core/morph-review/SKILL.md +1 -1
  22. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  23. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  24. package/framework/templates/.idea/morph-templates.xml +92 -0
  25. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  26. package/framework/templates/IDE-SNIPPETS.md +262 -262
  27. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  28. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  29. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  30. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  31. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  32. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  33. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  34. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  35. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  36. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  37. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  38. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  39. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  40. package/framework/templates/state.template.json +17 -17
  41. package/framework/templates/ui/design-system.css +226 -226
  42. package/package.json +1 -1
  43. package/src/commands/project/worktree.js +24 -2
  44. package/src/commands/state/finish.js +43 -1
  45. package/src/commands/state/report-check.js +36 -1
  46. package/src/commands/validation/verify.js +3 -0
  47. package/src/dashboard/app/main.jsx +13 -13
  48. package/src/dashboard/vite.config.js +24 -24
  49. package/src/lib/heavy/gate.js +22 -1
  50. package/src/lib/review-reports.js +5 -3
  51. package/src/lib/tasks/test-nodes.js +24 -6
  52. package/src/lib/tasks/test-runner.js +40 -1
  53. package/src/lib/validators/verify-runner.js +50 -12
  54. package/src/utils/agents-installer.js +1 -1
@@ -1,226 +1,226 @@
1
- /**
2
- * Design System - {Project Name}
3
- *
4
- * CSS Variables generated from design-system.md
5
- * Use estas variáveis em todo o projeto para manter consistência visual.
6
- *
7
- * Template MORPH-SPEC by Polymorphism Tech
8
- */
9
-
10
- :root {
11
- /* ========================================
12
- * CORES PRINCIPAIS
13
- * ======================================== */
14
-
15
- --color-primary: #{hex da cor primária};
16
- --color-primary-hover: #{hex da cor primária mais escura};
17
- --color-primary-active: #{hex da cor primária ainda mais escura};
18
-
19
- --color-secondary: #{hex da cor secundária};
20
- --color-secondary-hover: #{hex};
21
- --color-secondary-active: #{hex};
22
-
23
- /* ========================================
24
- * CORES DE ESTADO
25
- * ======================================== */
26
-
27
- --color-success: #{hex};
28
- --color-success-bg: #{hex com transparência ou shade mais claro};
29
- --color-success-border: #{hex};
30
-
31
- --color-error: #{hex};
32
- --color-error-bg: #{hex};
33
- --color-error-border: #{hex};
34
-
35
- --color-warning: #{hex};
36
- --color-warning-bg: #{hex};
37
- --color-warning-border: #{hex};
38
-
39
- --color-info: #{hex};
40
- --color-info-bg: #{hex};
41
- --color-info-border: #{hex};
42
-
43
- /* ========================================
44
- * ESCALA DE NEUTROS (Cinzas)
45
- * ======================================== */
46
-
47
- --color-gray-50: #{hex};
48
- --color-gray-100: #{hex};
49
- --color-gray-200: #{hex};
50
- --color-gray-300: #{hex};
51
- --color-gray-400: #{hex};
52
- --color-gray-500: #{hex};
53
- --color-gray-600: #{hex};
54
- --color-gray-700: #{hex};
55
- --color-gray-800: #{hex};
56
- --color-gray-900: #{hex};
57
-
58
- /* ========================================
59
- * TIPOGRAFIA - FONTES
60
- * ======================================== */
61
-
62
- --font-sans: '{Font Name}', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
63
- --font-serif: '{Font Name}', Georgia, 'Times New Roman', serif;
64
- --font-mono: '{Font Name}', 'Fira Code', 'Courier New', Consolas, monospace;
65
-
66
- /* ========================================
67
- * TIPOGRAFIA - TAMANHOS
68
- * ======================================== */
69
-
70
- --text-xs: 0.75rem; /* 12px */
71
- --text-sm: 0.875rem; /* 14px */
72
- --text-base: 1rem; /* 16px */
73
- --text-lg: 1.125rem; /* 18px */
74
- --text-xl: 1.25rem; /* 20px */
75
- --text-2xl: 1.5rem; /* 24px */
76
- --text-3xl: 1.875rem; /* 30px */
77
- --text-4xl: 2.25rem; /* 36px */
78
- --text-5xl: 3rem; /* 48px */
79
-
80
- /* ========================================
81
- * TIPOGRAFIA - LINE HEIGHTS
82
- * ======================================== */
83
-
84
- --leading-none: 1;
85
- --leading-tight: 1.25;
86
- --leading-snug: 1.375;
87
- --leading-normal: 1.5;
88
- --leading-relaxed: 1.625;
89
- --leading-loose: 2;
90
-
91
- /* ========================================
92
- * TIPOGRAFIA - FONT WEIGHTS
93
- * ======================================== */
94
-
95
- --font-light: 300;
96
- --font-regular: 400;
97
- --font-medium: 500;
98
- --font-semibold: 600;
99
- --font-bold: 700;
100
- --font-extrabold: 800;
101
-
102
- /* ========================================
103
- * ESPAÇAMENTO
104
- * Baseado em múltiplos de {base}px (ex: 4px)
105
- * ======================================== */
106
-
107
- --space-0: 0;
108
- --space-1: 0.25rem; /* 4px */
109
- --space-2: 0.5rem; /* 8px */
110
- --space-3: 0.75rem; /* 12px */
111
- --space-4: 1rem; /* 16px */
112
- --space-5: 1.25rem; /* 20px */
113
- --space-6: 1.5rem; /* 24px */
114
- --space-7: 1.75rem; /* 28px */
115
- --space-8: 2rem; /* 32px */
116
- --space-10: 2.5rem; /* 40px */
117
- --space-12: 3rem; /* 48px */
118
- --space-16: 4rem; /* 64px */
119
- --space-20: 5rem; /* 80px */
120
- --space-24: 6rem; /* 96px */
121
-
122
- /* ========================================
123
- * BORDER RADIUS
124
- * ======================================== */
125
-
126
- --rounded-none: 0;
127
- --rounded-sm: 0.125rem; /* 2px */
128
- --rounded-md: 0.375rem; /* 6px */
129
- --rounded-lg: 0.5rem; /* 8px */
130
- --rounded-xl: 0.75rem; /* 12px */
131
- --rounded-2xl: 1rem; /* 16px */
132
- --rounded-full: 9999px;
133
-
134
- /* ========================================
135
- * SOMBRAS (Shadows)
136
- * ======================================== */
137
-
138
- --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
139
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
140
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
141
- --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
142
- --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
143
-
144
- /* ========================================
145
- * Z-INDEX LAYERS
146
- * ======================================== */
147
-
148
- --z-0: 0;
149
- --z-10: 10;
150
- --z-20: 20;
151
- --z-30: 30;
152
- --z-40: 40;
153
- --z-50: 50; /* Modals, dropdowns */
154
- --z-100: 100; /* Tooltips */
155
- --z-max: 9999; /* Toasts, notifications */
156
-
157
- /* ========================================
158
- * TRANSITIONS / ANIMATIONS
159
- * ======================================== */
160
-
161
- --transition-fast: 150ms ease-in-out;
162
- --transition-base: 250ms ease-in-out;
163
- --transition-slow: 350ms ease-in-out;
164
-
165
- --ease-in: cubic-bezier(0.4, 0, 1, 1);
166
- --ease-out: cubic-bezier(0, 0, 0.2, 1);
167
- --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
168
-
169
- /* ========================================
170
- * BREAKPOINTS (informativo - usar em media queries)
171
- * ======================================== */
172
-
173
- /* @media (min-width: 640px) - sm */
174
- /* @media (min-width: 768px) - md */
175
- /* @media (min-width: 1024px) - lg */
176
- /* @media (min-width: 1280px) - xl */
177
- /* @media (min-width: 1536px) - 2xl */
178
- }
179
-
180
- /* ========================================
181
- * MODO ESCURO (Dark Mode)
182
- * Opcional - ajuste as cores para dark theme
183
- * ======================================== */
184
-
185
- @media (prefers-color-scheme: dark) {
186
- :root {
187
- /* Inverter cores conforme necessário */
188
- --color-gray-50: #{hex do gray-900};
189
- --color-gray-900: #{hex do gray-50};
190
- /* ... outras inversões */
191
- }
192
- }
193
-
194
- /* ========================================
195
- * CLASSES UTILITÁRIAS (Exemplo)
196
- * ======================================== */
197
-
198
- /* Exemplo de uso das variáveis */
199
- .btn-primary {
200
- background-color: var(--color-primary);
201
- color: white;
202
- padding: var(--space-2) var(--space-4);
203
- border-radius: var(--rounded-md);
204
- font-weight: var(--font-semibold);
205
- transition: background-color var(--transition-base);
206
- }
207
-
208
- .btn-primary:hover {
209
- background-color: var(--color-primary-hover);
210
- }
211
-
212
- .card {
213
- background-color: white;
214
- padding: var(--space-6);
215
- border-radius: var(--rounded-lg);
216
- box-shadow: var(--shadow-md);
217
- }
218
-
219
- .text-muted {
220
- color: var(--color-gray-600);
221
- font-size: var(--text-sm);
222
- }
223
-
224
- /* ========================================
225
- * FIM DO DESIGN SYSTEM
226
- * ======================================== */
1
+ /**
2
+ * Design System - {Project Name}
3
+ *
4
+ * CSS Variables generated from design-system.md
5
+ * Use estas variáveis em todo o projeto para manter consistência visual.
6
+ *
7
+ * Template MORPH-SPEC by Polymorphism Tech
8
+ */
9
+
10
+ :root {
11
+ /* ========================================
12
+ * CORES PRINCIPAIS
13
+ * ======================================== */
14
+
15
+ --color-primary: #{hex da cor primária};
16
+ --color-primary-hover: #{hex da cor primária mais escura};
17
+ --color-primary-active: #{hex da cor primária ainda mais escura};
18
+
19
+ --color-secondary: #{hex da cor secundária};
20
+ --color-secondary-hover: #{hex};
21
+ --color-secondary-active: #{hex};
22
+
23
+ /* ========================================
24
+ * CORES DE ESTADO
25
+ * ======================================== */
26
+
27
+ --color-success: #{hex};
28
+ --color-success-bg: #{hex com transparência ou shade mais claro};
29
+ --color-success-border: #{hex};
30
+
31
+ --color-error: #{hex};
32
+ --color-error-bg: #{hex};
33
+ --color-error-border: #{hex};
34
+
35
+ --color-warning: #{hex};
36
+ --color-warning-bg: #{hex};
37
+ --color-warning-border: #{hex};
38
+
39
+ --color-info: #{hex};
40
+ --color-info-bg: #{hex};
41
+ --color-info-border: #{hex};
42
+
43
+ /* ========================================
44
+ * ESCALA DE NEUTROS (Cinzas)
45
+ * ======================================== */
46
+
47
+ --color-gray-50: #{hex};
48
+ --color-gray-100: #{hex};
49
+ --color-gray-200: #{hex};
50
+ --color-gray-300: #{hex};
51
+ --color-gray-400: #{hex};
52
+ --color-gray-500: #{hex};
53
+ --color-gray-600: #{hex};
54
+ --color-gray-700: #{hex};
55
+ --color-gray-800: #{hex};
56
+ --color-gray-900: #{hex};
57
+
58
+ /* ========================================
59
+ * TIPOGRAFIA - FONTES
60
+ * ======================================== */
61
+
62
+ --font-sans: '{Font Name}', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
63
+ --font-serif: '{Font Name}', Georgia, 'Times New Roman', serif;
64
+ --font-mono: '{Font Name}', 'Fira Code', 'Courier New', Consolas, monospace;
65
+
66
+ /* ========================================
67
+ * TIPOGRAFIA - TAMANHOS
68
+ * ======================================== */
69
+
70
+ --text-xs: 0.75rem; /* 12px */
71
+ --text-sm: 0.875rem; /* 14px */
72
+ --text-base: 1rem; /* 16px */
73
+ --text-lg: 1.125rem; /* 18px */
74
+ --text-xl: 1.25rem; /* 20px */
75
+ --text-2xl: 1.5rem; /* 24px */
76
+ --text-3xl: 1.875rem; /* 30px */
77
+ --text-4xl: 2.25rem; /* 36px */
78
+ --text-5xl: 3rem; /* 48px */
79
+
80
+ /* ========================================
81
+ * TIPOGRAFIA - LINE HEIGHTS
82
+ * ======================================== */
83
+
84
+ --leading-none: 1;
85
+ --leading-tight: 1.25;
86
+ --leading-snug: 1.375;
87
+ --leading-normal: 1.5;
88
+ --leading-relaxed: 1.625;
89
+ --leading-loose: 2;
90
+
91
+ /* ========================================
92
+ * TIPOGRAFIA - FONT WEIGHTS
93
+ * ======================================== */
94
+
95
+ --font-light: 300;
96
+ --font-regular: 400;
97
+ --font-medium: 500;
98
+ --font-semibold: 600;
99
+ --font-bold: 700;
100
+ --font-extrabold: 800;
101
+
102
+ /* ========================================
103
+ * ESPAÇAMENTO
104
+ * Baseado em múltiplos de {base}px (ex: 4px)
105
+ * ======================================== */
106
+
107
+ --space-0: 0;
108
+ --space-1: 0.25rem; /* 4px */
109
+ --space-2: 0.5rem; /* 8px */
110
+ --space-3: 0.75rem; /* 12px */
111
+ --space-4: 1rem; /* 16px */
112
+ --space-5: 1.25rem; /* 20px */
113
+ --space-6: 1.5rem; /* 24px */
114
+ --space-7: 1.75rem; /* 28px */
115
+ --space-8: 2rem; /* 32px */
116
+ --space-10: 2.5rem; /* 40px */
117
+ --space-12: 3rem; /* 48px */
118
+ --space-16: 4rem; /* 64px */
119
+ --space-20: 5rem; /* 80px */
120
+ --space-24: 6rem; /* 96px */
121
+
122
+ /* ========================================
123
+ * BORDER RADIUS
124
+ * ======================================== */
125
+
126
+ --rounded-none: 0;
127
+ --rounded-sm: 0.125rem; /* 2px */
128
+ --rounded-md: 0.375rem; /* 6px */
129
+ --rounded-lg: 0.5rem; /* 8px */
130
+ --rounded-xl: 0.75rem; /* 12px */
131
+ --rounded-2xl: 1rem; /* 16px */
132
+ --rounded-full: 9999px;
133
+
134
+ /* ========================================
135
+ * SOMBRAS (Shadows)
136
+ * ======================================== */
137
+
138
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
139
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
140
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
141
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
142
+ --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
143
+
144
+ /* ========================================
145
+ * Z-INDEX LAYERS
146
+ * ======================================== */
147
+
148
+ --z-0: 0;
149
+ --z-10: 10;
150
+ --z-20: 20;
151
+ --z-30: 30;
152
+ --z-40: 40;
153
+ --z-50: 50; /* Modals, dropdowns */
154
+ --z-100: 100; /* Tooltips */
155
+ --z-max: 9999; /* Toasts, notifications */
156
+
157
+ /* ========================================
158
+ * TRANSITIONS / ANIMATIONS
159
+ * ======================================== */
160
+
161
+ --transition-fast: 150ms ease-in-out;
162
+ --transition-base: 250ms ease-in-out;
163
+ --transition-slow: 350ms ease-in-out;
164
+
165
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
166
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
167
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
168
+
169
+ /* ========================================
170
+ * BREAKPOINTS (informativo - usar em media queries)
171
+ * ======================================== */
172
+
173
+ /* @media (min-width: 640px) - sm */
174
+ /* @media (min-width: 768px) - md */
175
+ /* @media (min-width: 1024px) - lg */
176
+ /* @media (min-width: 1280px) - xl */
177
+ /* @media (min-width: 1536px) - 2xl */
178
+ }
179
+
180
+ /* ========================================
181
+ * MODO ESCURO (Dark Mode)
182
+ * Opcional - ajuste as cores para dark theme
183
+ * ======================================== */
184
+
185
+ @media (prefers-color-scheme: dark) {
186
+ :root {
187
+ /* Inverter cores conforme necessário */
188
+ --color-gray-50: #{hex do gray-900};
189
+ --color-gray-900: #{hex do gray-50};
190
+ /* ... outras inversões */
191
+ }
192
+ }
193
+
194
+ /* ========================================
195
+ * CLASSES UTILITÁRIAS (Exemplo)
196
+ * ======================================== */
197
+
198
+ /* Exemplo de uso das variáveis */
199
+ .btn-primary {
200
+ background-color: var(--color-primary);
201
+ color: white;
202
+ padding: var(--space-2) var(--space-4);
203
+ border-radius: var(--rounded-md);
204
+ font-weight: var(--font-semibold);
205
+ transition: background-color var(--transition-base);
206
+ }
207
+
208
+ .btn-primary:hover {
209
+ background-color: var(--color-primary-hover);
210
+ }
211
+
212
+ .card {
213
+ background-color: white;
214
+ padding: var(--space-6);
215
+ border-radius: var(--rounded-lg);
216
+ box-shadow: var(--shadow-md);
217
+ }
218
+
219
+ .text-muted {
220
+ color: var(--color-gray-600);
221
+ font-size: var(--text-sm);
222
+ }
223
+
224
+ /* ========================================
225
+ * FIM DO DESIGN SYSTEM
226
+ * ======================================== */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "9.7.0",
3
+ "version": "9.8.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -85,8 +85,27 @@ function getDateSuffix() {
85
85
  return new Date().toISOString().slice(0, 10).replace(/-/g, '');
86
86
  }
87
87
 
88
- // The ONLY links `finish` may auto-remove (never widened to any reparse point found), plus
89
- // `{project.frontendPath}/node_modules`: a monorepo frontend has its own install.
88
+ function trackedPackageNodeModules(mainCwd) {
89
+ let out;
90
+ try {
91
+ out = execFileSync('git', ['ls-files', '-z', '--', '*package.json'], { cwd: mainCwd, stdio: 'pipe' }).toString();
92
+ } catch {
93
+ return [];
94
+ }
95
+ const dirs = [];
96
+ for (const file of out.split('\0')) {
97
+ const parts = file.split('/');
98
+ if (parts.length < 2 || parts.at(-1) !== 'package.json') continue;
99
+ const dirParts = parts.slice(0, -1);
100
+ if (dirParts.includes('node_modules') || WORKTREE_DIR_NAMES.includes(dirParts[0])) continue;
101
+ const rel = join(...dirParts, 'node_modules');
102
+ if (existsSync(join(mainCwd, rel))) dirs.push(rel);
103
+ }
104
+ return dirs;
105
+ }
106
+
107
+ // The ONLY links `finish` may auto-remove (never widened to any reparse point found), plus the
108
+ // `node_modules` of each tracked nested package: a monorepo package has its own install.
90
109
  function resolveInfraLinkDirs(mainCwd) {
91
110
  const dirs = ['node_modules', '.claude'];
92
111
  if (existsSync(join(mainCwd, '.morph', 'framework'))) {
@@ -100,6 +119,9 @@ function resolveInfraLinkDirs(mainCwd) {
100
119
  if (frontendPath) dirs.push(join(frontendPath, 'node_modules'));
101
120
  }
102
121
  } catch {}
122
+ for (const rel of trackedPackageNodeModules(mainCwd)) {
123
+ if (!dirs.includes(rel)) dirs.push(rel);
124
+ }
103
125
  return dirs;
104
126
  }
105
127
 
@@ -292,7 +292,8 @@ export async function finishCommand(feature, options = {}) {
292
292
  }
293
293
  const dirtLines = withoutLeftoverIgnore(cleanCwd, dirty.split(/\r?\n/).filter((l) => l.trim()));
294
294
  const testArtifacts = untrackedTestArtifacts(dirtLines);
295
- if (dirtLines.length > testArtifacts.length) {
295
+ const commitState = dirtLines.length > testArtifacts.length && onlyFeatureStateDirty(cleanCwd, feature);
296
+ if (dirtLines.length > testArtifacts.length && !commitState) {
296
297
  emit({ finished: false, error: isWorktreeFlow ? 'worktree-dirty' : 'working-dir-dirty', hint: 'commit seu trabalho na branch antes do finish', feature });
297
298
  process.exit(1);
298
299
  }
@@ -341,6 +342,10 @@ export async function finishCommand(feature, options = {}) {
341
342
 
342
343
  // Every refusal is behind us: the leftover of a killed `graph refresh` goes now, never before.
343
344
  dropLeftoverIgnore(cleanCwd);
345
+ if (commitState && !commitFeatureState(cleanCwd, feature)) {
346
+ emit({ finished: false, error: 'state-commit-failed', hint: `não foi possível commitar .morph/features/${feature} (identidade git ausente?) — commite o estado na branch e rode o finish de novo`, feature });
347
+ process.exit(1);
348
+ }
344
349
  if (isWorktreeFlow) {
345
350
  for (const p of testArtifacts) rmSync(join(cleanCwd, p), { recursive: true, force: true });
346
351
  }
@@ -409,6 +414,15 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
409
414
 
410
415
  if (defaultBranch) out.defaultBranch = defaultBranch;
411
416
 
417
+ try {
418
+ const tree = worktreePath || root;
419
+ const dirt = gitStatusPorcelain(tree);
420
+ const work = dirt === null ? [] : withoutLeftoverIgnore(tree, dirt.split(/\r?\n/).filter((l) => l.trim()));
421
+ if (work.length > untrackedTestArtifacts(work).length && onlyFeatureStateDirty(tree, feature)) {
422
+ out.wouldDo.unshift(`commitar o estado sujo de .morph/features/${feature} (${stateCommitMessage(feature)}) na branch ${branch}`);
423
+ }
424
+ } catch { /* o pré-voo nunca lança */ }
425
+
412
426
  // A retenção automática também é mutação do finish real; a contagem é a de AGORA, antes do
413
427
  // backup que este finish gravar.
414
428
  try {
@@ -808,6 +822,34 @@ function untrackedTestArtifacts(lines) {
808
822
  .filter((p) => isTestArtifact(p));
809
823
  }
810
824
 
825
+ const stateCommitMessage = (feature) => `chore(morph): estado da feature ${feature}`;
826
+
827
+ // `--untracked-files=all`: o porcelain padrão colapsa `?? .morph/` e esconderia sujeira fora do escopo.
828
+ function onlyFeatureStateDirty(cwd, feature) {
829
+ let out;
830
+ try {
831
+ out = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd, stdio: 'pipe' }).toString();
832
+ } catch {
833
+ return false;
834
+ }
835
+ const scope = `.morph/features/${feature}/`;
836
+ const paths = out.split(/\r?\n/).filter((l) => l.trim())
837
+ .flatMap((l) => l.slice(3).split(' -> '))
838
+ .map((p) => p.replace(/^"|"$/g, ''));
839
+ return paths.length > 0 && paths.every((p) => p.startsWith(scope));
840
+ }
841
+
842
+ function commitFeatureState(cwd, feature) {
843
+ const scope = `.morph/features/${feature}`;
844
+ try {
845
+ execFileSync('git', ['add', '-A', '--', scope], { cwd, stdio: 'pipe' });
846
+ execFileSync('git', ['commit', '-q', '-m', stateCommitMessage(feature), '--', scope], { cwd, stdio: 'pipe' });
847
+ return true;
848
+ } catch {
849
+ return false;
850
+ }
851
+ }
852
+
811
853
  function gitStatusPorcelain(cwd) {
812
854
  try {
813
855
  return execFileSync('git', ['status', '--porcelain'], { cwd, stdio: 'pipe' }).toString();
@@ -15,6 +15,7 @@ import { verifierModelFor } from '../../lib/risk-detector.js';
15
15
  import { changedPathsAgainst, detectDefaultBranch } from '../../lib/git/default-branch.js';
16
16
  import { readMorphConfig } from '../../lib/tasks/test-runner.js';
17
17
  import { readPlanChecks } from '../../lib/plan/checks.js';
18
+ import { isTestPath } from '../../lib/graph/sanitize.js';
18
19
  import { featureGeneratesMedia, collectReviewRequirement } from './gate-check.js';
19
20
 
20
21
  export { inspectReport, REPORT_MARKERS };
@@ -139,12 +140,45 @@ function decide(report, observed, stale) {
139
140
  : { verdict: 'no-work-no-report', action: 'redispatch' };
140
141
  }
141
142
 
143
+ const isTestRef = (p) => isTestPath(p) || /\.(test|spec)\.[a-z]+$|Tests?\.cs$/i.test(p);
144
+
145
+ /** Cn whose `**Checks:**` entry cites tests in more than one file, as `{ id, files }`. */
146
+ export function checksSpread(body) {
147
+ const lines = String(body ?? '').split(/\r?\n/);
148
+ const start = lines.findIndex((l) => /^\s*(?:[-*]\s*)?\*\*checks\s*:?\*\*/i.test(l));
149
+ if (start < 0) return [];
150
+ const block = [lines[start].replace(/^.*?\*\*checks\s*:?\*\*\s*:?/i, '')];
151
+ for (const l of lines.slice(start + 1)) {
152
+ if (/^\s*(\*\*|#)/.test(l)) break;
153
+ block.push(l);
154
+ }
155
+ const filesByCn = new Map();
156
+ let current = null;
157
+ for (const piece of block.join('\n').split(/[;\n]/)) {
158
+ const lead = /^\s*(?:[-*]\s*)?\(?(C\d+)\b/.exec(piece);
159
+ if (lead) current = lead[1];
160
+ if (!current) continue;
161
+ const refs = (piece.match(/[\w@.\/\\-]+\.[A-Za-z]+/g) ?? []).map(slash).filter(isTestRef);
162
+ if (!filesByCn.has(current)) filesByCn.set(current, new Set());
163
+ for (const f of refs) filesByCn.get(current).add(f);
164
+ }
165
+ return [...filesByCn].filter(([, files]) => files.size > 1).map(([id, files]) => ({ id, files: [...files] }));
166
+ }
167
+
168
+ function spreadWarnings(fsPath) {
169
+ let body = '';
170
+ try { body = readFileSync(fsPath, 'utf8'); } catch { return []; }
171
+ return checksSpread(body).map(({ id, files }) =>
172
+ `${id} cita testes em ${files.length} arquivos (${files.join(', ')}): um teste por Cn, na camada mais baixa que o prova`);
173
+ }
174
+
142
175
  function checkSlice(cwd, feature, slice, telemetry) {
143
176
  const fsPath = sliceReportPath(join(cwd, '.morph', 'features', feature), slice.id);
144
177
  const report = inspectReport(fsPath, { covers: slice.covers ?? [] });
145
178
  const observed = observedEvidence(telemetry, feature, slice);
146
179
  const stale = report.exists && isReportStale(fsPath, observed);
147
- return { slice: slice.id, report, observed, ...decide(report, observed, stale) };
180
+ const warnings = report.exists ? spreadWarnings(fsPath) : [];
181
+ return { slice: slice.id, report, observed, warnings, ...decide(report, observed, stale) };
148
182
  }
149
183
 
150
184
  const ACTION_HINT = {
@@ -220,6 +254,7 @@ function printHuman(obj) {
220
254
  }
221
255
  console.log(chalk.gray(` → ${r.hint}`));
222
256
  }
257
+ for (const w of r.warnings ?? []) console.log(chalk.yellow(` ⚠ ${w}`));
223
258
  }
224
259
  for (const [role, r] of Object.entries(obj.reviews ?? {})) {
225
260
  if (!r.exists && role === 'security') continue;
@@ -309,6 +309,9 @@ export async function verifyCommand(featureName, options = {}) {
309
309
  for (const u of result.baseline.updates) {
310
310
  console.log(chalk.green(` baseline ${u.id}: ${u.from ?? '—'} → ${u.to}`));
311
311
  }
312
+ for (const id of result.baseline.removed ?? []) {
313
+ console.log(chalk.gray(` baseline ${id}: removida (runner sem baseline, só o piso zero)`));
314
+ }
312
315
  console.log(chalk.gray(`Gravada em ${result.baseline.path}
313
316
  `));
314
317
  }
@@ -1,13 +1,13 @@
1
- import { StrictMode } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { BrowserRouter } from 'react-router-dom';
4
- import App from './App.jsx';
5
- import './styles/index.css';
6
-
7
- createRoot(document.getElementById('root')).render(
8
- <StrictMode>
9
- <BrowserRouter>
10
- <App />
11
- </BrowserRouter>
12
- </StrictMode>
13
- );
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { BrowserRouter } from 'react-router-dom';
4
+ import App from './App.jsx';
5
+ import './styles/index.css';
6
+
7
+ createRoot(document.getElementById('root')).render(
8
+ <StrictMode>
9
+ <BrowserRouter>
10
+ <App />
11
+ </BrowserRouter>
12
+ </StrictMode>
13
+ );