@polymorphism-tech/morph-spec 4.3.0 → 4.3.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.
@@ -129,12 +129,15 @@ Based on detected patterns in your codebase:
129
129
  }
130
130
 
131
131
  if (config?.language === 'javascript') {
132
+ const techLine = config.technologies?.length > 0
133
+ ? `\n- **Framework**: ${config.technologies.join(', ')}`
134
+ : '';
135
+
132
136
  return `### JavaScript / TypeScript Conventions
133
137
 
134
138
  Based on detected patterns in your codebase:
135
139
 
136
- - **Package Manager**: ${config.packageManager}
137
- - **Framework**: ${config.technologies.join(', ')}
140
+ - **Package Manager**: ${config.packageManager}${techLine}
138
141
 
139
142
  **Recommendation**: Refer to framework standards for JavaScript best practices.`;
140
143
  }
@@ -149,6 +152,37 @@ function generateArchitectureSection(structure) {
149
152
  const { architecture } = structure;
150
153
 
151
154
  const descriptions = {
155
+ 'cli-library': `### CLI / Library Architecture
156
+
157
+ Your project follows a CLI/Library pattern:
158
+
159
+ - ✅ **bin/** entry points detected
160
+ - ✅ **src/** source directory detected
161
+ - ✅ **package.json** present
162
+
163
+ **Key principles**:
164
+ - Commands exposed via bin/
165
+ - Core logic in src/commands/ and src/lib/
166
+ - Public API exported from src/index.js`,
167
+
168
+ 'nextjs-app-router': `### Next.js App Router Architecture
169
+
170
+ Your project uses the Next.js App Router pattern:
171
+
172
+ - ✅ **app/** directory detected (React Server Components)
173
+
174
+ **Key principles**:
175
+ - Server Components by default
176
+ - Client Components with 'use client' directive
177
+ - Route handlers in app/api/`,
178
+
179
+ 'express-mvc': `### Express MVC Architecture
180
+
181
+ Your project uses Express with MVC pattern:
182
+
183
+ - ✅ **routes/** detected
184
+ - ✅ **controllers/** detected`,
185
+
152
186
  'clean-architecture': `### Clean Architecture
153
187
 
154
188
  Your project follows Clean Architecture pattern:
@@ -203,19 +237,29 @@ function generateRecommendations(structure, config, conversation) {
203
237
  }
204
238
  }
205
239
 
206
- // Recommend tests
207
- if (!structure?.patterns?.includes('Unit Tests')) {
208
- recommendations.push('No unit tests detected - consider adding test project');
209
- }
240
+ if (structure?.stack === 'nodejs' || config?.language === 'javascript') {
241
+ if (!structure?.patterns?.includes('Unit Tests')) {
242
+ recommendations.push('No unit tests detected - consider adding test coverage with Jest or Vitest');
243
+ }
210
244
 
211
- // Recommend DI
212
- if (!structure?.patterns?.includes('Dependency Injection')) {
213
- recommendations.push('Consider implementing Dependency Injection pattern');
214
- }
245
+ if (structure?.architecture === 'unknown') {
246
+ recommendations.push('No clear architecture detected - consider organizing code into src/commands/, src/lib/, src/core/');
247
+ }
248
+ } else {
249
+ // Recommend tests (generic / .NET)
250
+ if (!structure?.patterns?.includes('Unit Tests')) {
251
+ recommendations.push('No unit tests detected - consider adding test project');
252
+ }
215
253
 
216
- // Architecture recommendations
217
- if (structure?.architecture === 'unknown' && structure?.folders?.hasServices) {
218
- recommendations.push('Service layer detected but no clear architecture - consider Clean Architecture or CQRS');
254
+ // Recommend DI (only meaningful for C# / .NET)
255
+ if (config?.language === 'csharp' && !structure?.patterns?.includes('Dependency Injection')) {
256
+ recommendations.push('Consider implementing Dependency Injection pattern');
257
+ }
258
+
259
+ // Architecture recommendations (.NET)
260
+ if (structure?.architecture === 'unknown' && structure?.folders?.hasServices) {
261
+ recommendations.push('Service layer detected but no clear architecture - consider Clean Architecture or CQRS');
262
+ }
219
263
  }
220
264
 
221
265
  return recommendations;
@@ -250,14 +294,30 @@ function identifyGaps(structure, config) {
250
294
  */
251
295
  function filterImportantDependencies(deps) {
252
296
  const important = deps.filter(dep => {
253
- // Framework packages
297
+ // .NET Framework packages
254
298
  if (dep.includes('Microsoft.') || dep.includes('System.')) return true;
255
- // UI libraries
299
+ // Blazor UI libraries
256
300
  if (dep.includes('Blazor') || dep.includes('FluentUI') || dep.includes('Mud')) return true;
257
- // Common tools
301
+ // .NET common tools
258
302
  if (dep.includes('Hangfire') || dep.includes('Serilog') || dep.includes('AutoMapper')) return true;
259
- // AI/ML
303
+ // AI/ML (.NET)
260
304
  if (dep.includes('Agents') || dep.includes('AI') || dep.includes('OpenAI')) return true;
305
+ // JS: frontend frameworks
306
+ if (['next', 'react', 'react-dom', 'vue', 'nuxt', 'svelte', 'astro'].includes(dep)) return true;
307
+ // JS: backend frameworks
308
+ if (['express', 'fastify', 'hono', 'koa', 'nestjs', '@nestjs/core'].includes(dep)) return true;
309
+ // JS: databases / ORMs
310
+ if (['prisma', '@prisma/client', 'supabase', '@supabase/supabase-js', 'drizzle-orm', 'mongoose', 'typeorm'].includes(dep)) return true;
311
+ // JS: auth
312
+ if (['next-auth', 'clerk', '@clerk/nextjs', 'lucia', 'better-auth'].includes(dep)) return true;
313
+ // JS: validation / schema
314
+ if (['zod', 'yup', 'joi', 'valibot'].includes(dep)) return true;
315
+ // JS: testing
316
+ if (['vitest', 'jest', '@jest/core', 'mocha', 'cypress', 'playwright'].includes(dep)) return true;
317
+ // JS: UI / styling
318
+ if (['tailwindcss', '@mui/material', 'shadcn-ui', 'radix-ui', 'chakra-ui'].includes(dep)) return true;
319
+ // JS: AI
320
+ if (dep.includes('openai') || dep.includes('anthropic') || dep.includes('langchain') || dep.includes('ai')) return true;
261
321
  return false;
262
322
  });
263
323
 
@@ -97,9 +97,11 @@ async function detectStack(projectPath) {
97
97
  ]
98
98
  };
99
99
 
100
+ const globIgnore = ['stacks/**', 'framework/**', 'test/**', 'node_modules/**', '.morph/**'];
101
+
100
102
  for (const [stack, globs] of Object.entries(patterns)) {
101
103
  for (const pattern of globs) {
102
- const files = await glob(pattern, { cwd: projectPath, nodir: true });
104
+ const files = await glob(pattern, { cwd: projectPath, nodir: true, ignore: globIgnore });
103
105
  if (files.length > 0) {
104
106
  return stack;
105
107
  }
@@ -113,7 +115,10 @@ async function detectStack(projectPath) {
113
115
  * Detect architecture pattern
114
116
  */
115
117
  async function detectArchitecture(projectPath) {
118
+ const globIgnore = ['stacks/**', 'framework/**', 'test/**', 'node_modules/**', '.morph/**'];
119
+
116
120
  const checks = [
121
+ // .NET: Clean Architecture
117
122
  {
118
123
  pattern: 'clean-architecture',
119
124
  indicators: [
@@ -122,13 +127,15 @@ async function detectArchitecture(projectPath) {
122
127
  existsSync(join(projectPath, 'src', 'Infrastructure'))
123
128
  ]
124
129
  },
130
+ // .NET: CQRS
125
131
  {
126
132
  pattern: 'cqrs',
127
133
  indicators: [
128
- await glob('**/Commands/**/*.cs', { cwd: projectPath }).then(f => f.length > 0),
129
- await glob('**/Queries/**/*.cs', { cwd: projectPath }).then(f => f.length > 0)
134
+ await glob('**/Commands/**/*.cs', { cwd: projectPath, ignore: globIgnore }).then(f => f.length > 0),
135
+ await glob('**/Queries/**/*.cs', { cwd: projectPath, ignore: globIgnore }).then(f => f.length > 0)
130
136
  ]
131
137
  },
138
+ // .NET: MVC
132
139
  {
133
140
  pattern: 'mvc',
134
141
  indicators: [
@@ -136,6 +143,31 @@ async function detectArchitecture(projectPath) {
136
143
  existsSync(join(projectPath, 'Models')),
137
144
  existsSync(join(projectPath, 'Views'))
138
145
  ]
146
+ },
147
+ // JS: CLI / Library (bin + src + lib)
148
+ {
149
+ pattern: 'cli-library',
150
+ indicators: [
151
+ existsSync(join(projectPath, 'bin')),
152
+ existsSync(join(projectPath, 'src')),
153
+ existsSync(join(projectPath, 'package.json'))
154
+ ]
155
+ },
156
+ // JS: Next.js App Router
157
+ {
158
+ pattern: 'nextjs-app-router',
159
+ indicators: [
160
+ existsSync(join(projectPath, 'app')),
161
+ await glob('app/**/*.{js,ts,jsx,tsx}', { cwd: projectPath, ignore: globIgnore }).then(f => f.length > 0)
162
+ ]
163
+ },
164
+ // JS: Express MVC
165
+ {
166
+ pattern: 'express-mvc',
167
+ indicators: [
168
+ existsSync(join(projectPath, 'src', 'routes')) || existsSync(join(projectPath, 'routes')),
169
+ existsSync(join(projectPath, 'src', 'controllers')) || existsSync(join(projectPath, 'controllers'))
170
+ ]
139
171
  }
140
172
  ];
141
173
 
@@ -176,47 +208,43 @@ async function detectUILibrary(projectPath) {
176
208
  */
177
209
  async function detectPatterns(projectPath) {
178
210
  const patterns = [];
211
+ // Ignore template/vendor directories; keep test/ accessible for JS test detection
212
+ const globIgnore = ['stacks/**', 'framework/**', 'test/fixtures/**', 'node_modules/**', '.morph/**'];
179
213
 
180
- // Check for specific patterns
181
- const checks = [
182
- {
183
- name: 'Repository Pattern',
184
- glob: '**/*Repository.cs'
185
- },
186
- {
187
- name: 'Service Layer',
188
- glob: '**/*Service.cs'
189
- },
190
- {
191
- name: 'DTOs',
192
- glob: '**/*Dto.cs'
193
- },
194
- {
195
- name: 'Entity Framework',
196
- glob: '**/Migrations/**/*.cs'
197
- },
198
- {
199
- name: 'Dependency Injection',
200
- glob: '**/DependencyInjection.cs'
201
- },
202
- {
203
- name: 'Hangfire Jobs',
204
- glob: '**/*Job.cs'
205
- },
206
- {
207
- name: 'AI Agents',
208
- glob: '**/*Agent.cs'
209
- },
210
- {
211
- name: 'Unit Tests',
212
- glob: '**/*.Tests/**/*.cs'
213
- }
214
+ // C# / .NET patterns
215
+ const dotnetChecks = [
216
+ { name: 'Repository Pattern', glob: '**/*Repository.cs' },
217
+ { name: 'Service Layer', glob: '**/*Service.cs' },
218
+ { name: 'DTOs', glob: '**/*Dto.cs' },
219
+ { name: 'Entity Framework', glob: '**/Migrations/**/*.cs' },
220
+ { name: 'Dependency Injection', glob: '**/DependencyInjection.cs' },
221
+ { name: 'Hangfire Jobs', glob: '**/*Job.cs' },
222
+ { name: 'AI Agents', glob: '**/*Agent.cs' },
223
+ { name: 'Unit Tests', glob: '**/*.Tests/**/*.cs' }
224
+ ];
225
+
226
+ // JavaScript / TypeScript patterns
227
+ const jsChecks = [
228
+ { name: 'Service Layer', glob: 'src/**/*Service.js' },
229
+ { name: 'Service Layer', glob: 'src/**/*Service.ts' },
230
+ { name: 'Repository Pattern', glob: 'src/**/*Repository.js' },
231
+ { name: 'Repository Pattern', glob: 'src/**/*Repository.ts' },
232
+ { name: 'API Routes', glob: 'src/**/routes/**/*.{js,ts}' },
233
+ { name: 'API Routes', glob: 'src/commands/**/*.{js,ts}' },
234
+ { name: 'Unit Tests', glob: 'test/**/*.test.{js,ts}' },
235
+ { name: 'Unit Tests', glob: 'src/**/*.test.{js,ts}' },
236
+ { name: 'Unit Tests', glob: 'src/**/*.spec.{js,ts}' }
214
237
  ];
215
238
 
216
- for (const { name, glob: pattern } of checks) {
217
- const files = await glob(pattern, { cwd: projectPath, nodir: true });
239
+ const allChecks = [...dotnetChecks, ...jsChecks];
240
+ const seen = new Set();
241
+
242
+ for (const { name, glob: pattern } of allChecks) {
243
+ if (seen.has(name)) continue;
244
+ const files = await glob(pattern, { cwd: projectPath, nodir: true, ignore: globIgnore });
218
245
  if (files.length > 0) {
219
246
  patterns.push(name);
247
+ seen.add(name);
220
248
  }
221
249
  }
222
250
 
@@ -75,18 +75,38 @@ export async function generateRecap(projectPath, featureName, options = {}) {
75
75
  }
76
76
 
77
77
  function getTasksSummary(feature) {
78
- const tasks = Array.isArray(feature.tasks) ? feature.tasks : [];
79
- const completed = tasks.filter(t => t.status === 'completed');
80
- const pending = tasks.filter(t => t.status === 'pending');
81
- const inProgress = tasks.filter(t => t.status === 'in_progress');
78
+ // v2: feature.tasks is an array of task objects
79
+ if (Array.isArray(feature.tasks)) {
80
+ const tasks = feature.tasks;
81
+ const completed = tasks.filter(t => t.status === 'completed');
82
+ const pending = tasks.filter(t => t.status === 'pending');
83
+ const inProgress = tasks.filter(t => t.status === 'in_progress');
84
+ return {
85
+ total: tasks.length,
86
+ completed: completed.length,
87
+ pending: pending.length,
88
+ inProgress: inProgress.length,
89
+ percentage: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0,
90
+ items: tasks
91
+ };
92
+ }
93
+
94
+ // v3: feature.tasks is a counter object {total, completed, inProgress, pending}
95
+ // Use feature.taskList for individual items if available
96
+ const counters = feature.tasks || {};
97
+ const items = feature.taskList || [];
98
+ const total = counters.total || items.length || 0;
99
+ const completedCount = counters.completed ?? items.filter(t => t.status === 'completed').length;
100
+ const pendingCount = counters.pending ?? items.filter(t => t.status === 'pending').length;
101
+ const inProgressCount = counters.inProgress ?? items.filter(t => t.status === 'in_progress').length;
82
102
 
83
103
  return {
84
- total: tasks.length,
85
- completed: completed.length,
86
- pending: pending.length,
87
- inProgress: inProgress.length,
88
- percentage: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0,
89
- items: tasks
104
+ total,
105
+ completed: completedCount,
106
+ pending: pendingCount,
107
+ inProgress: inProgressCount,
108
+ percentage: total > 0 ? Math.round((completedCount / total) * 100) : 0,
109
+ items
90
110
  };
91
111
  }
92
112
 
@@ -187,39 +187,23 @@ async function runSingleValidator(validatorId, projectPath, featureName, options
187
187
  }
188
188
 
189
189
  case 'blazor': {
190
- try {
191
- const { validateBlazorPatterns } = await import('./blazor-validator.js');
192
- return await validateBlazorPatterns(projectPath, options);
193
- } catch {
194
- return null; // Validator may not exist or have different export
195
- }
190
+ const { validateBlazorPatterns } = await import('./blazor/blazor-validator.js');
191
+ return await validateBlazorPatterns(projectPath, options);
196
192
  }
197
193
 
198
194
  case 'blazor-concurrency': {
199
- try {
200
- const { analyzeConcurrency } = await import('./blazor-concurrency-analyzer.js');
201
- return await analyzeConcurrency(projectPath, options);
202
- } catch {
203
- return null;
204
- }
195
+ const { analyzeConcurrency } = await import('./blazor/blazor-concurrency-analyzer.js');
196
+ return await analyzeConcurrency(projectPath, options);
205
197
  }
206
198
 
207
199
  case 'blazor-state': {
208
- try {
209
- const { validateState } = await import('./blazor-state-validator.js');
210
- return await validateState(projectPath, options);
211
- } catch {
212
- return null;
213
- }
200
+ const { validateState } = await import('./blazor/blazor-state-validator.js');
201
+ return await validateState(projectPath, options);
214
202
  }
215
203
 
216
204
  case 'css': {
217
- try {
218
- const { validateCss } = await import('./css-validator.js');
219
- return await validateCss(projectPath, options);
220
- } catch {
221
- return null;
222
- }
205
+ const { validateCss } = await import('./css/css-validator.js');
206
+ return await validateCss(projectPath, options);
223
207
  }
224
208
 
225
209
  case 'contract-compliance': {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Claude Code Hooks Installer
3
+ *
4
+ * Merges the MORPH-SPEC agent-teams PostToolUse hook into
5
+ * .claude/settings.local.json without overwriting existing config.
6
+ *
7
+ * Called by `morph-spec init` and `morph-spec update`.
8
+ */
9
+
10
+ import { join } from 'path';
11
+ import { readFile, writeFile, mkdir } from 'fs/promises';
12
+ import { existsSync } from 'fs';
13
+
14
+ const DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
15
+
16
+ /**
17
+ * Install or update the MORPH-SPEC PostToolUse hook in .claude/settings.local.json.
18
+ *
19
+ * @param {string} targetPath - Project root directory
20
+ * @returns {boolean} true if the hook was newly added, false if it was already present
21
+ */
22
+ export async function installClaudeHooks(targetPath) {
23
+ const claudeDir = join(targetPath, '.claude');
24
+ const settingsPath = join(claudeDir, 'settings.local.json');
25
+
26
+ // Read existing settings or start empty
27
+ let settings = {};
28
+ if (existsSync(settingsPath)) {
29
+ try {
30
+ settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
31
+ } catch {
32
+ // Malformed JSON — start fresh to avoid corrupting the file
33
+ settings = {};
34
+ }
35
+ }
36
+
37
+ // Ensure hooks.PostToolUse array exists
38
+ settings.hooks = settings.hooks || {};
39
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
40
+
41
+ // Check if dispatch.js is already registered (avoid duplicate entries)
42
+ const alreadyRegistered = settings.hooks.PostToolUse.some(entry =>
43
+ Array.isArray(entry.hooks) &&
44
+ entry.hooks.some(h => h.command === DISPATCH_COMMAND)
45
+ );
46
+
47
+ if (alreadyRegistered) {
48
+ return false;
49
+ }
50
+
51
+ // Add the agent-teams dispatch hook
52
+ settings.hooks.PostToolUse.push({
53
+ matcher: 'Bash',
54
+ hooks: [
55
+ {
56
+ type: 'command',
57
+ command: DISPATCH_COMMAND
58
+ }
59
+ ]
60
+ });
61
+
62
+ // Ensure .claude directory exists
63
+ if (!existsSync(claudeDir)) {
64
+ await mkdir(claudeDir, { recursive: true });
65
+ }
66
+
67
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
68
+ return true;
69
+ }