@polymorphism-tech/morph-spec 3.1.0 → 3.2.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 (51) hide show
  1. package/CLAUDE.md +534 -0
  2. package/README.md +78 -4
  3. package/bin/morph-spec.js +50 -1
  4. package/bin/render-template.js +56 -10
  5. package/bin/task-manager.cjs +101 -7
  6. package/docs/cli-auto-detection.md +219 -0
  7. package/docs/llm-interaction-config.md +735 -0
  8. package/docs/troubleshooting.md +269 -0
  9. package/package.json +5 -1
  10. package/src/commands/advance-phase.js +93 -2
  11. package/src/commands/approve.js +221 -0
  12. package/src/commands/capture-pattern.js +121 -0
  13. package/src/commands/generate.js +128 -1
  14. package/src/commands/init.js +37 -0
  15. package/src/commands/migrate-state.js +158 -0
  16. package/src/commands/search-patterns.js +126 -0
  17. package/src/commands/spawn-team.js +172 -0
  18. package/src/commands/task.js +2 -2
  19. package/src/commands/update.js +36 -0
  20. package/src/commands/upgrade.js +346 -0
  21. package/src/generator/.gitkeep +0 -0
  22. package/src/generator/config-generator.js +206 -0
  23. package/src/generator/templates/config.json.template +40 -0
  24. package/src/generator/templates/project.md.template +67 -0
  25. package/src/lib/checkpoint-hooks.js +258 -0
  26. package/src/lib/metadata-extractor.js +380 -0
  27. package/src/lib/phase-state-machine.js +214 -0
  28. package/src/lib/state-manager.js +120 -0
  29. package/src/lib/template-data-sources.js +325 -0
  30. package/src/lib/validators/content-validator.js +351 -0
  31. package/src/llm/.gitkeep +0 -0
  32. package/src/llm/analyzer.js +215 -0
  33. package/src/llm/environment-detector.js +43 -0
  34. package/src/llm/few-shot-examples.js +216 -0
  35. package/src/llm/project-config-schema.json +188 -0
  36. package/src/llm/prompt-builder.js +96 -0
  37. package/src/llm/schema-validator.js +121 -0
  38. package/src/orchestrator.js +206 -0
  39. package/src/sanitizer/.gitkeep +0 -0
  40. package/src/sanitizer/context-sanitizer.js +221 -0
  41. package/src/sanitizer/patterns.js +163 -0
  42. package/src/scanner/.gitkeep +0 -0
  43. package/src/scanner/project-scanner.js +242 -0
  44. package/src/types/index.js +477 -0
  45. package/src/ui/.gitkeep +0 -0
  46. package/src/ui/diff-display.js +91 -0
  47. package/src/ui/interactive-wizard.js +96 -0
  48. package/src/ui/user-review.js +211 -0
  49. package/src/ui/wizard-questions.js +190 -0
  50. package/src/writer/.gitkeep +0 -0
  51. package/src/writer/file-writer.js +86 -0
@@ -139,6 +139,12 @@ function ensureFeature(featureName) {
139
139
  createdAt: new Date().toISOString(),
140
140
  updatedAt: new Date().toISOString(),
141
141
  activeAgents: [],
142
+ approvalGates: {
143
+ proposal: { approved: false, timestamp: null, approvedBy: null },
144
+ uiux: { approved: false, timestamp: null, approvedBy: null },
145
+ design: { approved: false, timestamp: null, approvedBy: null },
146
+ tasks: { approved: false, timestamp: null, approvedBy: null }
147
+ },
142
148
  outputs: {
143
149
  proposal: { created: false, path: `.morph/project/outputs/${featureName}/proposal.md` },
144
150
  spec: { created: false, path: `.morph/project/outputs/${featureName}/spec.md` },
@@ -412,3 +418,117 @@ export function getSummary() {
412
418
  featuresCount: Object.keys(state.features).length
413
419
  };
414
420
  }
421
+
422
+ // ============================================================================
423
+ // Approval Gates Operations
424
+ // ============================================================================
425
+
426
+ /**
427
+ * Set approval gate status
428
+ * @param {string} featureName - Feature name
429
+ * @param {string} gate - Gate name (proposal, uiux, design, tasks)
430
+ * @param {boolean} approved - Approval status
431
+ * @param {Object} metadata - Additional metadata (approvedBy, reason, etc.)
432
+ */
433
+ export function setApprovalGate(featureName, gate, approved, metadata = {}) {
434
+ const state = loadState();
435
+ const feature = ensureFeature(featureName);
436
+
437
+ if (!feature.approvalGates) {
438
+ feature.approvalGates = {
439
+ proposal: { approved: false, timestamp: null, approvedBy: null },
440
+ uiux: { approved: false, timestamp: null, approvedBy: null },
441
+ design: { approved: false, timestamp: null, approvedBy: null },
442
+ tasks: { approved: false, timestamp: null, approvedBy: null }
443
+ };
444
+ }
445
+
446
+ feature.approvalGates[gate] = {
447
+ approved,
448
+ timestamp: metadata.approvedAt || metadata.rejectedAt || new Date().toISOString(),
449
+ approvedBy: metadata.approvedBy || metadata.rejectedBy || null,
450
+ ...metadata
451
+ };
452
+
453
+ state.features[featureName] = feature;
454
+ saveState(state);
455
+ }
456
+
457
+ /**
458
+ * Get approval gate status
459
+ * @param {string} featureName - Feature name
460
+ * @param {string} gate - Gate name
461
+ * @returns {Object|null} Gate object or null
462
+ */
463
+ export function getApprovalGate(featureName, gate) {
464
+ const state = loadState();
465
+ const feature = state.features[featureName];
466
+
467
+ if (!feature || !feature.approvalGates) {
468
+ return null;
469
+ }
470
+
471
+ return feature.approvalGates[gate] || null;
472
+ }
473
+
474
+ /**
475
+ * Check if feature is pending approval
476
+ * @param {string} featureName - Feature name
477
+ * @returns {boolean} True if any gate is pending approval
478
+ */
479
+ export function isPendingApproval(featureName) {
480
+ const state = loadState();
481
+ const feature = state.features[featureName];
482
+
483
+ if (!feature || !feature.approvalGates) {
484
+ return false;
485
+ }
486
+
487
+ const currentPhase = feature.phase;
488
+
489
+ // Check if current phase has an approval gate
490
+ const phaseGateMap = {
491
+ 'design': 'design',
492
+ 'tasks': 'tasks',
493
+ 'uiux': 'uiux'
494
+ };
495
+
496
+ const relevantGate = phaseGateMap[currentPhase];
497
+ if (!relevantGate) {
498
+ return false;
499
+ }
500
+
501
+ const gate = feature.approvalGates[relevantGate];
502
+ return gate && !gate.approved;
503
+ }
504
+
505
+ /**
506
+ * Get approval history for a feature
507
+ * @param {string} featureName - Feature name
508
+ * @returns {Array} Array of approval events
509
+ */
510
+ export function getApprovalHistory(featureName) {
511
+ const state = loadState();
512
+ const feature = state.features[featureName];
513
+
514
+ if (!feature || !feature.approvalGates) {
515
+ return [];
516
+ }
517
+
518
+ const history = [];
519
+
520
+ Object.entries(feature.approvalGates).forEach(([gate, data]) => {
521
+ if (data.timestamp) {
522
+ history.push({
523
+ gate,
524
+ approved: data.approved,
525
+ timestamp: data.timestamp,
526
+ approvedBy: data.approvedBy || data.rejectedBy,
527
+ reason: data.reason
528
+ });
529
+ }
530
+ });
531
+
532
+ // Sort by timestamp
533
+ return history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
534
+ }
@@ -0,0 +1,325 @@
1
+ import { glob } from 'glob';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { execSync } from 'child_process';
5
+
6
+ /**
7
+ * Template Data Sources - Inject dynamic MCP data into templates
8
+ *
9
+ * Provides project context, compliance status, and recent activity
10
+ * for enriching template placeholders.
11
+ */
12
+
13
+ /**
14
+ * Get project structure statistics
15
+ * @param {string} projectPath - Project root path
16
+ * @returns {Promise<Object>} Project structure data
17
+ */
18
+ export async function getProjectStructure(projectPath = process.cwd()) {
19
+ try {
20
+ // Find all files (excluding common ignore patterns)
21
+ const files = await glob('**/*', {
22
+ cwd: projectPath,
23
+ ignore: [
24
+ 'node_modules/**',
25
+ 'bin/**',
26
+ 'obj/**',
27
+ '.git/**',
28
+ '.morph/**',
29
+ 'dist/**',
30
+ 'build/**',
31
+ '*.log'
32
+ ],
33
+ nodir: true
34
+ });
35
+
36
+ // Calculate language stats
37
+ const languageStats = {};
38
+ files.forEach(file => {
39
+ const ext = file.split('.').pop();
40
+ languageStats[ext] = (languageStats[ext] || 0) + 1;
41
+ });
42
+
43
+ // Get test coverage if available
44
+ let testCoverage = null;
45
+ const coveragePath = join(projectPath, 'coverage/coverage-summary.json');
46
+ if (existsSync(coveragePath)) {
47
+ const coverageData = JSON.parse(readFileSync(coveragePath, 'utf8'));
48
+ testCoverage = coverageData.total?.lines?.pct || null;
49
+ }
50
+
51
+ // Get last commit info
52
+ let lastCommit = null;
53
+ try {
54
+ const gitLog = execSync('git log -1 --format="%H|%an|%ar|%s"', {
55
+ cwd: projectPath,
56
+ encoding: 'utf8',
57
+ stdio: 'pipe'
58
+ });
59
+
60
+ const [hash, author, time, message] = gitLog.trim().split('|');
61
+ lastCommit = { hash: hash.substring(0, 7), author, time, message };
62
+ } catch {
63
+ // Git not available or not a repo
64
+ }
65
+
66
+ return {
67
+ totalFiles: files.length,
68
+ languageStats,
69
+ testCoverage: testCoverage ? Math.round(testCoverage) : null,
70
+ lastCommit,
71
+ filesByExtension: {
72
+ cs: languageStats.cs || 0,
73
+ js: languageStats.js || 0,
74
+ ts: languageStats.ts || 0,
75
+ tsx: languageStats.tsx || 0,
76
+ razor: languageStats.razor || 0,
77
+ css: languageStats.css || 0,
78
+ md: languageStats.md || 0
79
+ }
80
+ };
81
+ } catch (error) {
82
+ return {
83
+ error: error.message,
84
+ totalFiles: 0,
85
+ testCoverage: null
86
+ };
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get dependency information
92
+ * @param {string} projectPath - Project root path
93
+ * @returns {Promise<Object>} Dependency data
94
+ */
95
+ export async function getDependencyInfo(projectPath = process.cwd()) {
96
+ const dependencies = {
97
+ nuget: [],
98
+ npm: [],
99
+ outdated: []
100
+ };
101
+
102
+ try {
103
+ // Check for NuGet packages (C# projects)
104
+ const csprojFiles = await glob('**/*.csproj', {
105
+ cwd: projectPath,
106
+ ignore: ['**/bin/**', '**/obj/**']
107
+ });
108
+
109
+ if (csprojFiles.length > 0) {
110
+ const csprojPath = join(projectPath, csprojFiles[0]);
111
+ const csprojContent = readFileSync(csprojPath, 'utf8');
112
+
113
+ // Extract PackageReference elements
114
+ const packageRegex = /<PackageReference\s+Include="([^"]+)"\s+Version="([^"]+)"/g;
115
+ let match;
116
+
117
+ while ((match = packageRegex.exec(csprojContent)) !== null) {
118
+ dependencies.nuget.push({
119
+ name: match[1],
120
+ version: match[2]
121
+ });
122
+ }
123
+ }
124
+
125
+ // Check for npm packages
126
+ const packageJsonPath = join(projectPath, 'package.json');
127
+ if (existsSync(packageJsonPath)) {
128
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
129
+
130
+ const allDeps = {
131
+ ...packageJson.dependencies,
132
+ ...packageJson.devDependencies
133
+ };
134
+
135
+ Object.entries(allDeps).forEach(([name, version]) => {
136
+ dependencies.npm.push({ name, version });
137
+ });
138
+ }
139
+
140
+ } catch (error) {
141
+ dependencies.error = error.message;
142
+ }
143
+
144
+ return dependencies;
145
+ }
146
+
147
+ /**
148
+ * Get compliance status from validators
149
+ * @param {string} projectPath - Project root path
150
+ * @returns {Promise<Object>} Compliance data
151
+ */
152
+ export async function getComplianceStatus(projectPath = process.cwd()) {
153
+ const compliance = {
154
+ architectureViolations: 0,
155
+ packageConflicts: 0,
156
+ designSystemCompliance: 100,
157
+ securityIssues: 0,
158
+ overall: 100
159
+ };
160
+
161
+ try {
162
+ // Run validators if available
163
+ const validators = ['architecture', 'packages', 'design-system', 'security'];
164
+
165
+ for (const validator of validators) {
166
+ try {
167
+ const result = execSync(
168
+ `node bin/validate.js ${validator} --json`,
169
+ {
170
+ cwd: projectPath,
171
+ encoding: 'utf8',
172
+ stdio: 'pipe'
173
+ }
174
+ );
175
+
176
+ const parsed = JSON.parse(result);
177
+
178
+ if (validator === 'architecture') {
179
+ compliance.architectureViolations = parsed.errors || 0;
180
+ } else if (validator === 'packages') {
181
+ compliance.packageConflicts = parsed.errors || 0;
182
+ } else if (validator === 'design-system') {
183
+ const total = parsed.total || 100;
184
+ const errors = parsed.errors || 0;
185
+ compliance.designSystemCompliance = Math.round(((total - errors) / total) * 100);
186
+ } else if (validator === 'security') {
187
+ compliance.securityIssues = parsed.errors || 0;
188
+ }
189
+ } catch {
190
+ // Validator not available or failed - skip
191
+ }
192
+ }
193
+
194
+ // Calculate overall compliance score
195
+ const violations = compliance.architectureViolations +
196
+ compliance.packageConflicts +
197
+ compliance.securityIssues;
198
+
199
+ compliance.overall = Math.max(0, 100 - (violations * 5));
200
+
201
+ } catch (error) {
202
+ compliance.error = error.message;
203
+ }
204
+
205
+ return compliance;
206
+ }
207
+
208
+ /**
209
+ * Get recent activity and history
210
+ * @param {string} projectPath - Project root path
211
+ * @returns {Promise<Object>} Activity data
212
+ */
213
+ export async function getRecentActivity(projectPath = process.cwd()) {
214
+ const activity = {
215
+ lastFeature: null,
216
+ recentCommits: [],
217
+ activeBranches: [],
218
+ lastDeployment: null
219
+ };
220
+
221
+ try {
222
+ // Check state.json for last feature
223
+ const statePath = join(projectPath, '.morph/state.json');
224
+ if (existsSync(statePath)) {
225
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
226
+
227
+ // Find most recently updated feature
228
+ let lastFeature = null;
229
+ let lastTimestamp = null;
230
+
231
+ Object.entries(state.features || {}).forEach(([name, feature]) => {
232
+ const updatedAt = feature.updatedAt || feature.createdAt;
233
+ if (updatedAt && (!lastTimestamp || updatedAt > lastTimestamp)) {
234
+ lastTimestamp = updatedAt;
235
+ lastFeature = name;
236
+ }
237
+ });
238
+
239
+ activity.lastFeature = lastFeature;
240
+ }
241
+
242
+ // Get recent commits
243
+ try {
244
+ const gitLog = execSync('git log -5 --format="%h|%an|%ar|%s"', {
245
+ cwd: projectPath,
246
+ encoding: 'utf8',
247
+ stdio: 'pipe'
248
+ });
249
+
250
+ activity.recentCommits = gitLog.trim().split('\n').map(line => {
251
+ const [hash, author, time, message] = line.split('|');
252
+ return { hash, author, time, message };
253
+ });
254
+ } catch {
255
+ // Git not available
256
+ }
257
+
258
+ // Get active branches
259
+ try {
260
+ const branches = execSync('git branch -a', {
261
+ cwd: projectPath,
262
+ encoding: 'utf8',
263
+ stdio: 'pipe'
264
+ });
265
+
266
+ activity.activeBranches = branches
267
+ .split('\n')
268
+ .map(b => b.trim().replace(/^\*\s+/, ''))
269
+ .filter(b => b && !b.includes('->'));
270
+ } catch {
271
+ // Git not available
272
+ }
273
+
274
+ } catch (error) {
275
+ activity.error = error.message;
276
+ }
277
+
278
+ return activity;
279
+ }
280
+
281
+ /**
282
+ * Get all template data sources combined
283
+ * @param {string} projectPath - Project root path
284
+ * @returns {Promise<Object>} All data sources
285
+ */
286
+ export async function getAllTemplatePlaceholders(projectPath = process.cwd()) {
287
+ const [structure, dependencies, compliance, activity] = await Promise.all([
288
+ getProjectStructure(projectPath),
289
+ getDependencyInfo(projectPath),
290
+ getComplianceStatus(projectPath),
291
+ getRecentActivity(projectPath)
292
+ ]);
293
+
294
+ return {
295
+ MCP_PROJECT_FILES: structure.totalFiles,
296
+ MCP_TEST_COVERAGE: structure.testCoverage || 'N/A',
297
+ MCP_COMPLIANCE_SCORE: compliance.overall,
298
+ MCP_LAST_FEATURE: activity.lastFeature || 'None',
299
+ MCP_LAST_COMMIT: structure.lastCommit?.message || 'Unknown',
300
+ MCP_LAST_COMMIT_AUTHOR: structure.lastCommit?.author || 'Unknown',
301
+ MCP_LAST_COMMIT_TIME: structure.lastCommit?.time || 'Unknown',
302
+
303
+ MCP_CS_FILES: structure.filesByExtension?.cs || 0,
304
+ MCP_RAZOR_FILES: structure.filesByExtension?.razor || 0,
305
+ MCP_TS_FILES: structure.filesByExtension?.ts || 0,
306
+ MCP_JS_FILES: structure.filesByExtension?.js || 0,
307
+
308
+ MCP_NUGET_PACKAGES: dependencies.nuget.length,
309
+ MCP_NPM_PACKAGES: dependencies.npm.length,
310
+
311
+ MCP_ARCHITECTURE_VIOLATIONS: compliance.architectureViolations,
312
+ MCP_PACKAGE_CONFLICTS: compliance.packageConflicts,
313
+ MCP_SECURITY_ISSUES: compliance.securityIssues,
314
+
315
+ MCP_RECENT_BRANCHES: activity.activeBranches.slice(0, 3).join(', '),
316
+
317
+ // Full objects for advanced usage
318
+ _raw: {
319
+ structure,
320
+ dependencies,
321
+ compliance,
322
+ activity
323
+ }
324
+ };
325
+ }