@polymorphism-tech/morph-spec 2.3.0 → 3.0.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 (166) hide show
  1. package/CLAUDE.md +446 -1730
  2. package/README.md +515 -516
  3. package/bin/morph-spec.js +366 -294
  4. package/bin/task-manager.js +429 -368
  5. package/bin/validate.js +369 -268
  6. package/content/.claude/commands/morph-apply.md +221 -158
  7. package/content/.claude/commands/morph-deploy.md +529 -0
  8. package/content/.claude/commands/morph-preflight.md +227 -0
  9. package/content/.claude/commands/morph-proposal.md +122 -101
  10. package/content/.claude/commands/morph-status.md +86 -86
  11. package/content/.claude/commands/morph-troubleshoot.md +122 -0
  12. package/content/.claude/skills/infra/azure-deploy-specialist.md +699 -0
  13. package/content/.claude/skills/level-0-meta/README.md +7 -0
  14. package/content/.claude/skills/level-0-meta/code-review.md +226 -0
  15. package/content/.claude/skills/level-0-meta/morph-checklist.md +117 -0
  16. package/content/.claude/skills/level-0-meta/simulation-checklist.md +77 -0
  17. package/content/.claude/skills/level-1-workflows/README.md +7 -0
  18. package/content/.claude/skills/level-1-workflows/morph-replicate.md +213 -0
  19. package/content/.claude/{commands/morph-clarify.md → skills/level-1-workflows/phase-clarify.md} +131 -184
  20. package/content/.claude/{commands/morph-design.md → skills/level-1-workflows/phase-design.md} +213 -275
  21. package/content/.claude/skills/level-1-workflows/phase-setup.md +106 -0
  22. package/content/.claude/skills/level-1-workflows/phase-tasks.md +164 -0
  23. package/content/.claude/{commands/morph-uiux.md → skills/level-1-workflows/phase-uiux.md} +169 -211
  24. package/content/.claude/skills/level-2-domains/README.md +14 -0
  25. package/content/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +192 -0
  26. package/content/.claude/skills/{specialists → level-2-domains/architecture}/po-pm-advisor.md +197 -197
  27. package/content/.claude/skills/level-2-domains/architecture/standards-architect.md +156 -0
  28. package/content/.claude/skills/level-2-domains/backend/dotnet-senior.md +287 -0
  29. package/content/.claude/skills/level-2-domains/backend/ef-modeler.md +113 -0
  30. package/content/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +126 -0
  31. package/content/.claude/skills/level-2-domains/backend/ms-agent-expert.md +109 -0
  32. package/content/.claude/skills/level-2-domains/frontend/blazor-builder.md +210 -0
  33. package/content/.claude/skills/level-2-domains/frontend/nextjs-expert.md +154 -0
  34. package/content/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +191 -0
  35. package/content/.claude/skills/{specialists → level-2-domains/infrastructure}/azure-architect.md +142 -142
  36. package/content/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +126 -0
  37. package/content/.claude/skills/level-2-domains/infrastructure/container-specialist.md +131 -0
  38. package/content/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +119 -0
  39. package/content/.claude/skills/level-2-domains/integrations/asaas-financial.md +130 -0
  40. package/content/.claude/skills/level-2-domains/integrations/azure-identity.md +142 -0
  41. package/content/.claude/skills/level-2-domains/integrations/clerk-auth.md +108 -0
  42. package/content/.claude/skills/level-2-domains/integrations/resend-email.md +119 -0
  43. package/content/.claude/skills/level-2-domains/quality/code-analyzer.md +235 -0
  44. package/content/.claude/skills/level-2-domains/quality/testing-specialist.md +126 -0
  45. package/content/.claude/skills/level-3-technologies/README.md +7 -0
  46. package/content/.claude/skills/level-4-patterns/README.md +7 -0
  47. package/content/.claude/skills/specialists/prompt-engineer.md +189 -0
  48. package/content/.claude/skills/specialists/seo-growth-hacker.md +320 -0
  49. package/content/.morph/config/agents.json +762 -242
  50. package/content/.morph/config/config.template.json +122 -108
  51. package/content/.morph/docs/workflows/design-impl.md +37 -0
  52. package/content/.morph/docs/workflows/enforcement-pipeline.md +668 -0
  53. package/content/.morph/docs/workflows/fast-track.md +29 -0
  54. package/content/.morph/docs/workflows/full-morph.md +76 -0
  55. package/content/.morph/docs/workflows/standard.md +44 -0
  56. package/content/.morph/docs/workflows/ui-refresh.md +39 -0
  57. package/content/.morph/examples/scheduled-reports/decisions.md +158 -0
  58. package/content/.morph/examples/scheduled-reports/proposal.md +95 -0
  59. package/content/.morph/examples/scheduled-reports/spec.md +267 -0
  60. package/content/.morph/hooks/README.md +348 -239
  61. package/content/.morph/hooks/pre-commit-agents.sh +24 -24
  62. package/content/.morph/hooks/task-completed.js +73 -0
  63. package/content/.morph/hooks/teammate-idle.js +68 -0
  64. package/content/.morph/schemas/tasks.schema.json +220 -0
  65. package/content/.morph/standards/agent-framework-blazor-ui.md +359 -0
  66. package/content/.morph/standards/agent-framework-production.md +410 -0
  67. package/content/.morph/standards/agent-framework-setup.md +413 -453
  68. package/content/.morph/standards/agent-framework-workflows.md +349 -0
  69. package/content/.morph/standards/agent-teams-workflow.md +474 -0
  70. package/content/.morph/standards/architecture.md +325 -325
  71. package/content/.morph/standards/azure.md +605 -379
  72. package/content/.morph/standards/dotnet10-migration.md +520 -494
  73. package/content/.morph/templates/CONTEXT-FEATURE.md +276 -0
  74. package/content/.morph/templates/CONTEXT.md +170 -0
  75. package/content/.morph/templates/agent.cs +163 -172
  76. package/content/.morph/templates/clarify-questions.md +159 -0
  77. package/content/.morph/templates/contracts/Commands.cs +74 -0
  78. package/content/.morph/templates/contracts/Entities.cs +25 -0
  79. package/content/.morph/templates/contracts/Queries.cs +74 -0
  80. package/content/.morph/templates/contracts/README.md +74 -0
  81. package/content/.morph/templates/decisions.md +123 -106
  82. package/content/.morph/templates/infra/azure-pipelines-deploy.yml +480 -0
  83. package/content/.morph/templates/infra/deploy-checklist.md +426 -0
  84. package/content/.morph/templates/proposal.md +141 -155
  85. package/content/.morph/templates/recap.md +94 -105
  86. package/content/.morph/templates/simulation.md +353 -0
  87. package/content/.morph/templates/spec.md +149 -148
  88. package/content/.morph/templates/state.template.json +222 -222
  89. package/content/.morph/templates/tasks.md +257 -235
  90. package/content/.morph/templates/ui-components.md +362 -276
  91. package/content/CLAUDE.md +150 -442
  92. package/detectors/structure-detector.js +245 -250
  93. package/docs/README.md +144 -149
  94. package/docs/getting-started.md +301 -302
  95. package/docs/installation.md +361 -361
  96. package/docs/validation-checklist.md +265 -266
  97. package/package.json +80 -80
  98. package/src/commands/advance-phase.js +266 -0
  99. package/src/commands/analyze-blazor-concurrency.js +193 -0
  100. package/src/commands/deploy.js +780 -0
  101. package/src/commands/detect-agents.js +167 -0
  102. package/src/commands/doctor.js +356 -280
  103. package/src/commands/generate-context.js +40 -0
  104. package/src/commands/init.js +258 -245
  105. package/src/commands/lint-fluent.js +352 -0
  106. package/src/commands/rollback-phase.js +185 -0
  107. package/src/commands/session-summary.js +291 -0
  108. package/src/commands/task.js +78 -75
  109. package/src/commands/troubleshoot.js +222 -0
  110. package/src/commands/update.js +192 -159
  111. package/src/commands/validate-blazor-state.js +210 -0
  112. package/src/commands/validate-blazor.js +156 -0
  113. package/src/commands/validate-css.js +84 -0
  114. package/src/commands/validate-phase.js +221 -0
  115. package/src/lib/blazor-concurrency-analyzer.js +288 -0
  116. package/src/lib/blazor-state-validator.js +291 -0
  117. package/src/lib/blazor-validator.js +374 -0
  118. package/src/lib/complexity-analyzer.js +441 -292
  119. package/src/lib/context-generator.js +513 -0
  120. package/src/lib/continuous-validator.js +421 -440
  121. package/src/lib/css-validator.js +352 -0
  122. package/src/lib/decision-constraint-loader.js +109 -0
  123. package/src/lib/design-system-detector.js +187 -0
  124. package/src/lib/design-system-scaffolder.js +299 -0
  125. package/src/lib/hook-executor.js +256 -0
  126. package/src/lib/recap-generator.js +205 -0
  127. package/src/lib/spec-validator.js +258 -0
  128. package/src/lib/standards-context-injector.js +287 -0
  129. package/src/lib/state-manager.js +397 -340
  130. package/src/lib/team-orchestrator.js +322 -0
  131. package/src/lib/troubleshoot-grep.js +194 -0
  132. package/src/lib/troubleshoot-index.js +144 -0
  133. package/src/lib/validation-runner.js +283 -0
  134. package/src/lib/validators/contract-compliance-validator.js +273 -0
  135. package/src/lib/validators/design-system-validator.js +231 -0
  136. package/src/utils/file-copier.js +187 -139
  137. package/content/.claude/commands/morph-costs.md +0 -206
  138. package/content/.claude/commands/morph-setup.md +0 -100
  139. package/content/.claude/commands/morph-tasks.md +0 -319
  140. package/content/.claude/skills/infra/bicep-architect.md +0 -419
  141. package/content/.claude/skills/infra/container-specialist.md +0 -437
  142. package/content/.claude/skills/infra/devops-engineer.md +0 -405
  143. package/content/.claude/skills/integrations/asaas-financial.md +0 -333
  144. package/content/.claude/skills/integrations/azure-identity.md +0 -309
  145. package/content/.claude/skills/integrations/clerk-auth.md +0 -290
  146. package/content/.claude/skills/specialists/ai-system-architect.md +0 -604
  147. package/content/.claude/skills/specialists/cost-guardian.md +0 -110
  148. package/content/.claude/skills/specialists/ef-modeler.md +0 -211
  149. package/content/.claude/skills/specialists/hangfire-orchestrator.md +0 -255
  150. package/content/.claude/skills/specialists/ms-agent-expert.md +0 -263
  151. package/content/.claude/skills/specialists/standards-architect.md +0 -78
  152. package/content/.claude/skills/specialists/ui-ux-designer.md +0 -1100
  153. package/content/.claude/skills/stacks/dotnet-blazor.md +0 -606
  154. package/content/.claude/skills/stacks/dotnet-nextjs.md +0 -402
  155. package/content/.claude/skills/stacks/shopify.md +0 -445
  156. package/content/.morph/config/azure-pricing.json +0 -70
  157. package/content/.morph/config/azure-pricing.schema.json +0 -50
  158. package/content/.morph/hooks/pre-commit-costs.sh +0 -91
  159. package/docs/api/cost-calculator.js.html +0 -513
  160. package/docs/api/design-system-generator.js.html +0 -382
  161. package/docs/api/global.html +0 -5263
  162. package/docs/api/index.html +0 -96
  163. package/docs/api/state-manager.js.html +0 -423
  164. package/src/commands/cost.js +0 -181
  165. package/src/commands/update-pricing.js +0 -206
  166. package/src/lib/cost-calculator.js +0 -429
@@ -1,440 +1,421 @@
1
- /**
2
- * Continuous Validator
3
- *
4
- * Background watcher that validates project in real-time.
5
- * Detects issues before they become problems.
6
- *
7
- * MORPH-SPEC 3.0 - Sprint 4
8
- */
9
-
10
- import { readFileSync, existsSync, watch } from 'fs';
11
- import { glob } from 'glob';
12
- import { join, dirname } from 'path';
13
- import chalk from 'chalk';
14
-
15
- export class ContinuousValidator {
16
- constructor(projectPath = '.') {
17
- this.projectPath = projectPath;
18
- this.watchers = [];
19
- this.validationInterval = null;
20
- }
21
-
22
- /**
23
- * Start continuous validation
24
- */
25
- async start(options = {}) {
26
- const { watchMode = true, interval = 30000 } = options;
27
-
28
- console.log(chalk.cyan('🔍 Starting continuous validation...'));
29
-
30
- // Initial validation
31
- await this.runAllValidations();
32
-
33
- if (watchMode) {
34
- // Watch for file changes
35
- this.setupFileWatchers();
36
-
37
- // Periodic validation
38
- this.validationInterval = setInterval(() => {
39
- this.runAllValidations();
40
- }, interval);
41
-
42
- console.log(chalk.gray(`Watching for changes (interval: ${interval/1000}s)...`));
43
- }
44
- }
45
-
46
- /**
47
- * Stop continuous validation
48
- */
49
- stop() {
50
- // Stop file watchers
51
- this.watchers.forEach(watcher => watcher.close());
52
- this.watchers = [];
53
-
54
- // Stop periodic validation
55
- if (this.validationInterval) {
56
- clearInterval(this.validationInterval);
57
- this.validationInterval = null;
58
- }
59
-
60
- console.log(chalk.gray('Stopped continuous validation'));
61
- }
62
-
63
- /**
64
- * Setup file watchers
65
- */
66
- setupFileWatchers() {
67
- const watchPaths = [
68
- '**/*.csproj',
69
- '**/Program.cs',
70
- 'wwwroot/css/**/*.css',
71
- 'infra/**/*.bicep'
72
- ];
73
-
74
- for (const pattern of watchPaths) {
75
- const watcher = watch(pattern, { persistent: false }, async (eventType, filename) => {
76
- if (filename) {
77
- console.log(chalk.gray(`File changed: ${filename}`));
78
- await this.runAllValidations();
79
- }
80
- });
81
-
82
- this.watchers.push(watcher);
83
- }
84
- }
85
-
86
- /**
87
- * Run all validations
88
- */
89
- async runAllValidations() {
90
- const results = await Promise.all([
91
- this.validatePackageCompatibility(),
92
- this.validateArchitecturePatterns(),
93
- this.validateProgramCs(),
94
- this.validateUIContrast(),
95
- this.validateCosts()
96
- ]);
97
-
98
- const errors = results.filter(r => r.level === 'error');
99
- const warnings = results.filter(r => r.level === 'warning');
100
- const infos = results.filter(r => r.level === 'info');
101
-
102
- // Display summary
103
- if (errors.length === 0 && warnings.length === 0) {
104
- console.log(chalk.green('✅ All validations passed'));
105
- } else {
106
- if (errors.length > 0) {
107
- console.log(chalk.red(`\n❌ ${errors.length} error(s) found:`));
108
- errors.forEach(e => console.log(chalk.red(` - ${e.message}`)));
109
- }
110
-
111
- if (warnings.length > 0) {
112
- console.log(chalk.yellow(`\n⚠️ ${warnings.length} warning(s):`));
113
- warnings.forEach(w => console.log(chalk.yellow(` - ${w.message}`)));
114
- }
115
- }
116
-
117
- // Auto-fix errors if possible
118
- const fixableErrors = errors.filter(e => e.autoFix);
119
- if (fixableErrors.length > 0) {
120
- console.log(chalk.cyan(`\n🔧 Auto-fixing ${fixableErrors.length} issue(s)...`));
121
- for (const error of fixableErrors) {
122
- try {
123
- await error.autoFix();
124
- console.log(chalk.green(` ✅ Fixed: ${error.message}`));
125
- } catch (ex) {
126
- console.log(chalk.red(` ❌ Failed to fix: ${error.message}`));
127
- }
128
- }
129
- }
130
-
131
- return { errors, warnings, infos };
132
- }
133
-
134
- /**
135
- * Validate package compatibility (.NET 10)
136
- */
137
- async validatePackageCompatibility() {
138
- try {
139
- const csprojFiles = await glob('**/*.csproj', { ignore: '**/obj/**' });
140
-
141
- for (const file of csprojFiles) {
142
- const content = readFileSync(file, 'utf-8');
143
-
144
- // Extract .NET version
145
- const tfmMatch = content.match(/<TargetFramework>(.*?)<\/TargetFramework>/);
146
- if (!tfmMatch) continue;
147
-
148
- const targetFramework = tfmMatch[1];
149
- const dotnetVersion = parseInt(targetFramework.replace('net', ''));
150
-
151
- if (dotnetVersion < 10) {
152
- return {
153
- level: 'info',
154
- message: `${file}: Using .NET ${dotnetVersion} (consider upgrading to .NET 10)`
155
- };
156
- }
157
-
158
- // Package compatibility matrix
159
- const incompatiblePackages = this.checkPackageCompatibility(content, dotnetVersion);
160
-
161
- if (incompatiblePackages.length > 0) {
162
- return {
163
- level: 'error',
164
- message: `${file}: Incompatible packages with .NET ${dotnetVersion}:\n` +
165
- incompatiblePackages.map(p => ` - ${p.name} ${p.version} → upgrade to ${p.requiredVersion}`).join('\n'),
166
- packages: incompatiblePackages
167
- };
168
- }
169
- }
170
-
171
- return { level: 'ok' };
172
- } catch (error) {
173
- return { level: 'error', message: `Package validation failed: ${error.message}` };
174
- }
175
- }
176
-
177
- /**
178
- * Check package compatibility
179
- */
180
- checkPackageCompatibility(csprojContent, dotnetVersion) {
181
- const incompatible = [];
182
-
183
- // Compatibility matrix (from dotnet10-compatibility.md)
184
- const matrix = {
185
- 'MudBlazor': {
186
- 10: '8.15.0',
187
- pattern: /<PackageReference Include="MudBlazor" Version="(.*?)"/
188
- },
189
- 'Microsoft.FluentUI.AspNetCore.Components': {
190
- 10: '5.0.0',
191
- pattern: /<PackageReference Include="Microsoft\.FluentUI\.AspNetCore\.Components" Version="(.*?)"/
192
- },
193
- 'Hangfire.AspNetCore': {
194
- 10: '1.8.22',
195
- pattern: /<PackageReference Include="Hangfire\.AspNetCore" Version="(.*?)"/
196
- }
197
- };
198
-
199
- for (const [pkg, config] of Object.entries(matrix)) {
200
- const match = csprojContent.match(config.pattern);
201
- if (match) {
202
- const installedVersion = match[1];
203
- const requiredVersion = config[dotnetVersion];
204
-
205
- if (requiredVersion && this.compareVersions(installedVersion, requiredVersion) < 0) {
206
- incompatible.push({
207
- name: pkg,
208
- version: installedVersion,
209
- requiredVersion
210
- });
211
- }
212
- }
213
- }
214
-
215
- return incompatible;
216
- }
217
-
218
- /**
219
- * Compare semantic versions
220
- */
221
- compareVersions(v1, v2) {
222
- const parts1 = v1.split('.').map(Number);
223
- const parts2 = v2.split('.').map(Number);
224
-
225
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
226
- const p1 = parts1[i] || 0;
227
- const p2 = parts2[i] || 0;
228
- if (p1 < p2) return -1;
229
- if (p1 > p2) return 1;
230
- }
231
-
232
- return 0;
233
- }
234
-
235
- /**
236
- * Validate architecture patterns
237
- */
238
- async validateArchitecturePatterns() {
239
- try {
240
- const blazorFiles = await glob('**/*.razor.cs', { ignore: '**/obj/**' });
241
-
242
- for (const file of blazorFiles) {
243
- const content = readFileSync(file, 'utf-8');
244
-
245
- // Check for Application layer injection (anti-pattern)
246
- if (/\[Inject\].*I\w+Command/.test(content)) {
247
- return {
248
- level: 'error',
249
- message: `${file}: Blazor component injecting Application layer directly (use HttpClient pattern)`,
250
- reference: 'framework/standards/blazor-pitfalls.md'
251
- };
252
- }
253
-
254
- // Check for DbContext injection (anti-pattern)
255
- if (/\[Inject\].*DbContext/.test(content)) {
256
- return {
257
- level: 'error',
258
- message: `${file}: Blazor component injecting DbContext directly (use HttpClient → API → Handler)`,
259
- reference: 'framework/standards/blazor-pitfalls.md'
260
- };
261
- }
262
-
263
- // Check for JSRuntime in OnInitialized (pitfall)
264
- if (/OnInitialized.*JSRuntime/.test(content.replace(/\s+/g, ''))) {
265
- return {
266
- level: 'warning',
267
- message: `${file}: Possible JSRuntime call in OnInitialized (use OnAfterRenderAsync)`,
268
- reference: 'framework/standards/blazor-lifecycle.md'
269
- };
270
- }
271
- }
272
-
273
- return { level: 'ok' };
274
- } catch (error) {
275
- return { level: 'error', message: `Architecture validation failed: ${error.message}` };
276
- }
277
- }
278
-
279
- /**
280
- * Validate Program.cs setup
281
- */
282
- async validateProgramCs() {
283
- try {
284
- const programCs = 'Program.cs';
285
- if (!existsSync(programCs)) {
286
- return { level: 'ok' }; // Not a Blazor project
287
- }
288
-
289
- const content = readFileSync(programCs, 'utf-8');
290
-
291
- const checks = [
292
- { pattern: /app\.UseStaticFiles\(\)/, message: 'app.UseStaticFiles() is missing', critical: true },
293
- { pattern: /AddHttpClient\(\)/, message: 'AddHttpClient() is missing', critical: true },
294
- { pattern: /AddHttpContextAccessor\(\)/, message: 'AddHttpContextAccessor() is missing', critical: true }
295
- ];
296
-
297
- for (const check of checks) {
298
- if (!check.pattern.test(content)) {
299
- return {
300
- level: check.critical ? 'error' : 'warning',
301
- message: `Program.cs: ${check.message}`,
302
- reference: 'framework/standards/program-cs-checklist.md'
303
- };
304
- }
305
- }
306
-
307
- // Check middleware order (UseStaticFiles before UseAntiforgery)
308
- const staticFilesIndex = content.indexOf('UseStaticFiles');
309
- const antiforgeryIndex = content.indexOf('UseAntiforgery');
310
-
311
- if (staticFilesIndex > 0 && antiforgeryIndex > 0 && staticFilesIndex > antiforgeryIndex) {
312
- return {
313
- level: 'error',
314
- message: 'Program.cs: app.UseStaticFiles() must come BEFORE app.UseAntiforgery()',
315
- reference: 'framework/standards/program-cs-checklist.md'
316
- };
317
- }
318
-
319
- return { level: 'ok' };
320
- } catch (error) {
321
- return { level: 'error', message: `Program.cs validation failed: ${error.message}` };
322
- }
323
- }
324
-
325
- /**
326
- * Validate UI contrast (WCAG 2.1 AA)
327
- */
328
- async validateUIContrast() {
329
- try {
330
- const cssFiles = await glob('wwwroot/css/**/*.css', { ignore: '**/node_modules/**' });
331
-
332
- for (const file of cssFiles) {
333
- const content = readFileSync(file, 'utf-8');
334
-
335
- // Extract color variables
336
- const colors = this.extractColors(content);
337
-
338
- // Check common color pairs
339
- const issues = [];
340
- const bgColors = colors.filter(c => c.name.includes('background') || c.name.includes('bg'));
341
- const textColors = colors.filter(c => c.name.includes('text') || c.name.includes('color'));
342
-
343
- for (const bg of bgColors) {
344
- for (const text of textColors) {
345
- const ratio = this.calculateContrastRatio(bg.value, text.value);
346
- if (ratio < 4.5) { // WCAG AA minimum for text
347
- issues.push({ bg: bg.name, text: text.name, ratio: ratio.toFixed(2) });
348
- }
349
- }
350
- }
351
-
352
- if (issues.length > 0) {
353
- return {
354
- level: 'warning',
355
- message: `${file}: Low contrast detected (WCAG AA requires 4.5:1):\n` +
356
- issues.map(i => ` - ${i.text} on ${i.bg}: ${i.ratio}:1`).join('\n')
357
- };
358
- }
359
- }
360
-
361
- return { level: 'ok' };
362
- } catch (error) {
363
- return { level: 'ok' }; // CSS parsing is optional
364
- }
365
- }
366
-
367
- /**
368
- * Extract colors from CSS
369
- */
370
- extractColors(css) {
371
- const colors = [];
372
- const hexPattern = /--([\w-]+):\s*(#[0-9a-fA-F]{3,6})/g;
373
- let match;
374
-
375
- while ((match = hexPattern.exec(css)) !== null) {
376
- colors.push({ name: match[1], value: match[2] });
377
- }
378
-
379
- return colors;
380
- }
381
-
382
- /**
383
- * Calculate contrast ratio (WCAG formula)
384
- */
385
- calculateContrastRatio(color1, color2) {
386
- const l1 = this.getLuminance(color1);
387
- const l2 = this.getLuminance(color2);
388
-
389
- const lighter = Math.max(l1, l2);
390
- const darker = Math.min(l1, l2);
391
-
392
- return (lighter + 0.05) / (darker + 0.05);
393
- }
394
-
395
- /**
396
- * Get relative luminance
397
- */
398
- getLuminance(hex) {
399
- const rgb = this.hexToRgb(hex);
400
- if (!rgb) return 0;
401
-
402
- const [r, g, b] = rgb.map(val => {
403
- val = val / 255;
404
- return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
405
- });
406
-
407
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
408
- }
409
-
410
- /**
411
- * Convert hex to RGB
412
- */
413
- hexToRgb(hex) {
414
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
415
- return result ? [
416
- parseInt(result[1], 16),
417
- parseInt(result[2], 16),
418
- parseInt(result[3], 16)
419
- ] : null;
420
- }
421
-
422
- /**
423
- * Validate costs (Bicep files)
424
- */
425
- async validateCosts() {
426
- try {
427
- const bicepFiles = await glob('infra/**/*.bicep');
428
-
429
- if (bicepFiles.length === 0) {
430
- return { level: 'ok' }; // No infrastructure
431
- }
432
-
433
- // This would integrate with cost-calculator.js
434
- // For now, just check if files exist
435
- return { level: 'info', message: `Found ${bicepFiles.length} Bicep file(s) - run 'npx morph-spec cost' to estimate costs` };
436
- } catch (error) {
437
- return { level: 'ok' };
438
- }
439
- }
440
- }
1
+ /**
2
+ * Continuous Validator
3
+ *
4
+ * Background watcher that validates project in real-time.
5
+ * Detects issues before they become problems.
6
+ *
7
+ * MORPH-SPEC 3.0 - Sprint 4
8
+ */
9
+
10
+ import { readFileSync, existsSync, watch } from 'fs';
11
+ import { glob } from 'glob';
12
+ import { join, dirname } from 'path';
13
+ import chalk from 'chalk';
14
+
15
+ export class ContinuousValidator {
16
+ constructor(projectPath = '.') {
17
+ this.projectPath = projectPath;
18
+ this.watchers = [];
19
+ this.validationInterval = null;
20
+ }
21
+
22
+ /**
23
+ * Start continuous validation
24
+ */
25
+ async start(options = {}) {
26
+ const { watchMode = true, interval = 30000 } = options;
27
+
28
+ console.log(chalk.cyan('🔍 Starting continuous validation...'));
29
+
30
+ // Initial validation
31
+ await this.runAllValidations();
32
+
33
+ if (watchMode) {
34
+ // Watch for file changes
35
+ this.setupFileWatchers();
36
+
37
+ // Periodic validation
38
+ this.validationInterval = setInterval(() => {
39
+ this.runAllValidations();
40
+ }, interval);
41
+
42
+ console.log(chalk.gray(`Watching for changes (interval: ${interval/1000}s)...`));
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Stop continuous validation
48
+ */
49
+ stop() {
50
+ // Stop file watchers
51
+ this.watchers.forEach(watcher => watcher.close());
52
+ this.watchers = [];
53
+
54
+ // Stop periodic validation
55
+ if (this.validationInterval) {
56
+ clearInterval(this.validationInterval);
57
+ this.validationInterval = null;
58
+ }
59
+
60
+ console.log(chalk.gray('Stopped continuous validation'));
61
+ }
62
+
63
+ /**
64
+ * Setup file watchers
65
+ */
66
+ setupFileWatchers() {
67
+ const watchPaths = [
68
+ '**/*.csproj',
69
+ '**/Program.cs',
70
+ 'wwwroot/css/**/*.css',
71
+ 'infra/**/*.bicep'
72
+ ];
73
+
74
+ for (const pattern of watchPaths) {
75
+ const watcher = watch(pattern, { persistent: false }, async (eventType, filename) => {
76
+ if (filename) {
77
+ console.log(chalk.gray(`File changed: ${filename}`));
78
+ await this.runAllValidations();
79
+ }
80
+ });
81
+
82
+ this.watchers.push(watcher);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Run all validations
88
+ */
89
+ async runAllValidations() {
90
+ const results = await Promise.all([
91
+ this.validatePackageCompatibility(),
92
+ this.validateArchitecturePatterns(),
93
+ this.validateProgramCs(),
94
+ this.validateUIContrast()
95
+ ]);
96
+
97
+ const errors = results.filter(r => r.level === 'error');
98
+ const warnings = results.filter(r => r.level === 'warning');
99
+ const infos = results.filter(r => r.level === 'info');
100
+
101
+ // Display summary
102
+ if (errors.length === 0 && warnings.length === 0) {
103
+ console.log(chalk.green('✅ All validations passed'));
104
+ } else {
105
+ if (errors.length > 0) {
106
+ console.log(chalk.red(`\n❌ ${errors.length} error(s) found:`));
107
+ errors.forEach(e => console.log(chalk.red(` - ${e.message}`)));
108
+ }
109
+
110
+ if (warnings.length > 0) {
111
+ console.log(chalk.yellow(`\n⚠️ ${warnings.length} warning(s):`));
112
+ warnings.forEach(w => console.log(chalk.yellow(` - ${w.message}`)));
113
+ }
114
+ }
115
+
116
+ // Auto-fix errors if possible
117
+ const fixableErrors = errors.filter(e => e.autoFix);
118
+ if (fixableErrors.length > 0) {
119
+ console.log(chalk.cyan(`\n🔧 Auto-fixing ${fixableErrors.length} issue(s)...`));
120
+ for (const error of fixableErrors) {
121
+ try {
122
+ await error.autoFix();
123
+ console.log(chalk.green(` ✅ Fixed: ${error.message}`));
124
+ } catch (ex) {
125
+ console.log(chalk.red(` ❌ Failed to fix: ${error.message}`));
126
+ }
127
+ }
128
+ }
129
+
130
+ return { errors, warnings, infos };
131
+ }
132
+
133
+ /**
134
+ * Validate package compatibility (.NET 10)
135
+ */
136
+ async validatePackageCompatibility() {
137
+ try {
138
+ const csprojFiles = await glob('**/*.csproj', { ignore: '**/obj/**' });
139
+
140
+ for (const file of csprojFiles) {
141
+ const content = readFileSync(file, 'utf-8');
142
+
143
+ // Extract .NET version
144
+ const tfmMatch = content.match(/<TargetFramework>(.*?)<\/TargetFramework>/);
145
+ if (!tfmMatch) continue;
146
+
147
+ const targetFramework = tfmMatch[1];
148
+ const dotnetVersion = parseInt(targetFramework.replace('net', ''));
149
+
150
+ if (dotnetVersion < 10) {
151
+ return {
152
+ level: 'info',
153
+ message: `${file}: Using .NET ${dotnetVersion} (consider upgrading to .NET 10)`
154
+ };
155
+ }
156
+
157
+ // Package compatibility matrix
158
+ const incompatiblePackages = this.checkPackageCompatibility(content, dotnetVersion);
159
+
160
+ if (incompatiblePackages.length > 0) {
161
+ return {
162
+ level: 'error',
163
+ message: `${file}: Incompatible packages with .NET ${dotnetVersion}:\n` +
164
+ incompatiblePackages.map(p => ` - ${p.name} ${p.version} upgrade to ${p.requiredVersion}`).join('\n'),
165
+ packages: incompatiblePackages
166
+ };
167
+ }
168
+ }
169
+
170
+ return { level: 'ok' };
171
+ } catch (error) {
172
+ return { level: 'error', message: `Package validation failed: ${error.message}` };
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Check package compatibility
178
+ */
179
+ checkPackageCompatibility(csprojContent, dotnetVersion) {
180
+ const incompatible = [];
181
+
182
+ // Compatibility matrix (from dotnet10-compatibility.md)
183
+ const matrix = {
184
+ 'MudBlazor': {
185
+ 10: '8.15.0',
186
+ pattern: /<PackageReference Include="MudBlazor" Version="(.*?)"/
187
+ },
188
+ 'Microsoft.FluentUI.AspNetCore.Components': {
189
+ 10: '5.0.0',
190
+ pattern: /<PackageReference Include="Microsoft\.FluentUI\.AspNetCore\.Components" Version="(.*?)"/
191
+ },
192
+ 'Hangfire.AspNetCore': {
193
+ 10: '1.8.22',
194
+ pattern: /<PackageReference Include="Hangfire\.AspNetCore" Version="(.*?)"/
195
+ }
196
+ };
197
+
198
+ for (const [pkg, config] of Object.entries(matrix)) {
199
+ const match = csprojContent.match(config.pattern);
200
+ if (match) {
201
+ const installedVersion = match[1];
202
+ const requiredVersion = config[dotnetVersion];
203
+
204
+ if (requiredVersion && this.compareVersions(installedVersion, requiredVersion) < 0) {
205
+ incompatible.push({
206
+ name: pkg,
207
+ version: installedVersion,
208
+ requiredVersion
209
+ });
210
+ }
211
+ }
212
+ }
213
+
214
+ return incompatible;
215
+ }
216
+
217
+ /**
218
+ * Compare semantic versions
219
+ */
220
+ compareVersions(v1, v2) {
221
+ const parts1 = v1.split('.').map(Number);
222
+ const parts2 = v2.split('.').map(Number);
223
+
224
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
225
+ const p1 = parts1[i] || 0;
226
+ const p2 = parts2[i] || 0;
227
+ if (p1 < p2) return -1;
228
+ if (p1 > p2) return 1;
229
+ }
230
+
231
+ return 0;
232
+ }
233
+
234
+ /**
235
+ * Validate architecture patterns
236
+ */
237
+ async validateArchitecturePatterns() {
238
+ try {
239
+ const blazorFiles = await glob('**/*.razor.cs', { ignore: '**/obj/**' });
240
+
241
+ for (const file of blazorFiles) {
242
+ const content = readFileSync(file, 'utf-8');
243
+
244
+ // Check for Application layer injection (anti-pattern)
245
+ if (/\[Inject\].*I\w+Command/.test(content)) {
246
+ return {
247
+ level: 'error',
248
+ message: `${file}: Blazor component injecting Application layer directly (use HttpClient pattern)`,
249
+ reference: 'framework/standards/blazor-pitfalls.md'
250
+ };
251
+ }
252
+
253
+ // Check for DbContext injection (anti-pattern)
254
+ if (/\[Inject\].*DbContext/.test(content)) {
255
+ return {
256
+ level: 'error',
257
+ message: `${file}: Blazor component injecting DbContext directly (use HttpClient → API → Handler)`,
258
+ reference: 'framework/standards/blazor-pitfalls.md'
259
+ };
260
+ }
261
+
262
+ // Check for JSRuntime in OnInitialized (pitfall)
263
+ if (/OnInitialized.*JSRuntime/.test(content.replace(/\s+/g, ''))) {
264
+ return {
265
+ level: 'warning',
266
+ message: `${file}: Possible JSRuntime call in OnInitialized (use OnAfterRenderAsync)`,
267
+ reference: 'framework/standards/blazor-lifecycle.md'
268
+ };
269
+ }
270
+ }
271
+
272
+ return { level: 'ok' };
273
+ } catch (error) {
274
+ return { level: 'error', message: `Architecture validation failed: ${error.message}` };
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Validate Program.cs setup
280
+ */
281
+ async validateProgramCs() {
282
+ try {
283
+ const programCs = 'Program.cs';
284
+ if (!existsSync(programCs)) {
285
+ return { level: 'ok' }; // Not a Blazor project
286
+ }
287
+
288
+ const content = readFileSync(programCs, 'utf-8');
289
+
290
+ const checks = [
291
+ { pattern: /app\.UseStaticFiles\(\)/, message: 'app.UseStaticFiles() is missing', critical: true },
292
+ { pattern: /AddHttpClient\(\)/, message: 'AddHttpClient() is missing', critical: true },
293
+ { pattern: /AddHttpContextAccessor\(\)/, message: 'AddHttpContextAccessor() is missing', critical: true }
294
+ ];
295
+
296
+ for (const check of checks) {
297
+ if (!check.pattern.test(content)) {
298
+ return {
299
+ level: check.critical ? 'error' : 'warning',
300
+ message: `Program.cs: ${check.message}`,
301
+ reference: 'framework/standards/program-cs-checklist.md'
302
+ };
303
+ }
304
+ }
305
+
306
+ // Check middleware order (UseStaticFiles before UseAntiforgery)
307
+ const staticFilesIndex = content.indexOf('UseStaticFiles');
308
+ const antiforgeryIndex = content.indexOf('UseAntiforgery');
309
+
310
+ if (staticFilesIndex > 0 && antiforgeryIndex > 0 && staticFilesIndex > antiforgeryIndex) {
311
+ return {
312
+ level: 'error',
313
+ message: 'Program.cs: app.UseStaticFiles() must come BEFORE app.UseAntiforgery()',
314
+ reference: 'framework/standards/program-cs-checklist.md'
315
+ };
316
+ }
317
+
318
+ return { level: 'ok' };
319
+ } catch (error) {
320
+ return { level: 'error', message: `Program.cs validation failed: ${error.message}` };
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Validate UI contrast (WCAG 2.1 AA)
326
+ */
327
+ async validateUIContrast() {
328
+ try {
329
+ const cssFiles = await glob('wwwroot/css/**/*.css', { ignore: '**/node_modules/**' });
330
+
331
+ for (const file of cssFiles) {
332
+ const content = readFileSync(file, 'utf-8');
333
+
334
+ // Extract color variables
335
+ const colors = this.extractColors(content);
336
+
337
+ // Check common color pairs
338
+ const issues = [];
339
+ const bgColors = colors.filter(c => c.name.includes('background') || c.name.includes('bg'));
340
+ const textColors = colors.filter(c => c.name.includes('text') || c.name.includes('color'));
341
+
342
+ for (const bg of bgColors) {
343
+ for (const text of textColors) {
344
+ const ratio = this.calculateContrastRatio(bg.value, text.value);
345
+ if (ratio < 4.5) { // WCAG AA minimum for text
346
+ issues.push({ bg: bg.name, text: text.name, ratio: ratio.toFixed(2) });
347
+ }
348
+ }
349
+ }
350
+
351
+ if (issues.length > 0) {
352
+ return {
353
+ level: 'warning',
354
+ message: `${file}: Low contrast detected (WCAG AA requires 4.5:1):\n` +
355
+ issues.map(i => ` - ${i.text} on ${i.bg}: ${i.ratio}:1`).join('\n')
356
+ };
357
+ }
358
+ }
359
+
360
+ return { level: 'ok' };
361
+ } catch (error) {
362
+ return { level: 'ok' }; // CSS parsing is optional
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Extract colors from CSS
368
+ */
369
+ extractColors(css) {
370
+ const colors = [];
371
+ const hexPattern = /--([\w-]+):\s*(#[0-9a-fA-F]{3,6})/g;
372
+ let match;
373
+
374
+ while ((match = hexPattern.exec(css)) !== null) {
375
+ colors.push({ name: match[1], value: match[2] });
376
+ }
377
+
378
+ return colors;
379
+ }
380
+
381
+ /**
382
+ * Calculate contrast ratio (WCAG formula)
383
+ */
384
+ calculateContrastRatio(color1, color2) {
385
+ const l1 = this.getLuminance(color1);
386
+ const l2 = this.getLuminance(color2);
387
+
388
+ const lighter = Math.max(l1, l2);
389
+ const darker = Math.min(l1, l2);
390
+
391
+ return (lighter + 0.05) / (darker + 0.05);
392
+ }
393
+
394
+ /**
395
+ * Get relative luminance
396
+ */
397
+ getLuminance(hex) {
398
+ const rgb = this.hexToRgb(hex);
399
+ if (!rgb) return 0;
400
+
401
+ const [r, g, b] = rgb.map(val => {
402
+ val = val / 255;
403
+ return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
404
+ });
405
+
406
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
407
+ }
408
+
409
+ /**
410
+ * Convert hex to RGB
411
+ */
412
+ hexToRgb(hex) {
413
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
414
+ return result ? [
415
+ parseInt(result[1], 16),
416
+ parseInt(result[2], 16),
417
+ parseInt(result[3], 16)
418
+ ] : null;
419
+ }
420
+
421
+ }