@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
@@ -0,0 +1,206 @@
1
+ /**
2
+ * @fileoverview ConfigGenerator - Renders templates and generates config files
3
+ * @module morph-spec/generator/config-generator
4
+ */
5
+
6
+ import { readFile, access, copyFile } from 'fs/promises';
7
+ import { join, dirname } from 'path';
8
+ import Handlebars from 'handlebars';
9
+ import { fileURLToPath } from 'url';
10
+ import Ajv from 'ajv';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+
15
+ /**
16
+ * @typedef {import('../types/index.js').ProjectConfig} ProjectConfig
17
+ * @typedef {import('../types/index.js').GeneratedConfigs} GeneratedConfigs
18
+ */
19
+
20
+ /**
21
+ * Validation Error
22
+ */
23
+ export class ValidationError extends Error {
24
+ constructor(message, field, value) {
25
+ super(message);
26
+ this.name = 'ValidationError';
27
+ this.field = field;
28
+ this.value = value;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * ConfigGenerator - Generates configuration files from ProjectConfig
34
+ * @class
35
+ */
36
+ export class ConfigGenerator {
37
+ constructor() {
38
+ this.projectMdTemplate = null;
39
+ this.configJsonTemplate = null;
40
+ this.ajv = new Ajv({ allErrors: true });
41
+ }
42
+
43
+ /**
44
+ * Load templates from filesystem
45
+ * @private
46
+ */
47
+ async loadTemplates() {
48
+ if (this.projectMdTemplate && this.configJsonTemplate) {
49
+ return; // Already loaded
50
+ }
51
+
52
+ const templatesDir = join(__dirname, 'templates');
53
+
54
+ const [projectMdSource, configJsonSource] = await Promise.all([
55
+ readFile(join(templatesDir, 'project.md.template'), 'utf-8'),
56
+ readFile(join(templatesDir, 'config.json.template'), 'utf-8')
57
+ ]);
58
+
59
+ this.projectMdTemplate = Handlebars.compile(projectMdSource);
60
+ this.configJsonTemplate = Handlebars.compile(configJsonSource);
61
+ }
62
+
63
+ /**
64
+ * Generate configuration files from project config
65
+ * @param {ProjectConfig} projectConfig - Detected project config
66
+ * @returns {Promise<GeneratedConfigs>}
67
+ */
68
+ async generate(projectConfig) {
69
+ // Load templates if not already loaded
70
+ await this.loadTemplates();
71
+
72
+ // Add generation timestamp
73
+ const context = {
74
+ ...projectConfig,
75
+ generatedAt: new Date().toISOString()
76
+ };
77
+
78
+ // Render templates
79
+ const projectMd = this.renderProjectMd(context);
80
+ const configJson = this.renderConfigJson(context);
81
+
82
+ // Parse config.json to object
83
+ const configObject = JSON.parse(configJson);
84
+
85
+ // Validate config.json (optional, but good practice)
86
+ // Note: agent-schema.json validation would happen here if we had the schema
87
+ // For now, we just ensure it's valid JSON
88
+
89
+ return {
90
+ projectMd,
91
+ configJson,
92
+ configObject
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Render project.md template
98
+ * @param {ProjectConfig} config - Project config
99
+ * @returns {string} Rendered markdown
100
+ */
101
+ renderProjectMd(config) {
102
+ if (!this.projectMdTemplate) {
103
+ throw new Error('Templates not loaded. Call loadTemplates() first.');
104
+ }
105
+
106
+ return this.projectMdTemplate(config);
107
+ }
108
+
109
+ /**
110
+ * Render config.json template
111
+ * @param {ProjectConfig} config - Project config
112
+ * @returns {string} Rendered JSON string
113
+ */
114
+ renderConfigJson(config) {
115
+ if (!this.configJsonTemplate) {
116
+ throw new Error('Templates not loaded. Call loadTemplates() first.');
117
+ }
118
+
119
+ const rendered = this.configJsonTemplate(config);
120
+
121
+ // Validate that rendered output is valid JSON
122
+ try {
123
+ JSON.parse(rendered);
124
+ return rendered;
125
+ } catch (error) {
126
+ throw new ValidationError(
127
+ `Rendered config.json is not valid JSON: ${error.message}`,
128
+ 'configJson',
129
+ rendered
130
+ );
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Validate config.json against agent schema
136
+ * @param {string} configJson - JSON string
137
+ * @returns {boolean} True if valid
138
+ * @throws {ValidationError} If validation fails
139
+ */
140
+ validateConfigJson(configJson) {
141
+ // Parse JSON
142
+ let parsed;
143
+ try {
144
+ parsed = JSON.parse(configJson);
145
+ } catch (error) {
146
+ throw new ValidationError(
147
+ `Invalid JSON: ${error.message}`,
148
+ 'configJson',
149
+ configJson
150
+ );
151
+ }
152
+
153
+ // Basic validation - ensure required fields exist
154
+ const requiredFields = ['name', 'type', 'description', 'stack', 'architecture'];
155
+ for (const field of requiredFields) {
156
+ if (!parsed[field]) {
157
+ throw new ValidationError(
158
+ `Missing required field: ${field}`,
159
+ field,
160
+ parsed
161
+ );
162
+ }
163
+ }
164
+
165
+ // Validate stack.backend is required
166
+ if (!parsed.stack || !parsed.stack.backend) {
167
+ throw new ValidationError(
168
+ 'stack.backend is required',
169
+ 'stack.backend',
170
+ parsed
171
+ );
172
+ }
173
+
174
+ return true;
175
+ }
176
+
177
+ /**
178
+ * Backup existing configuration files
179
+ * @param {string} cwd - Current working directory
180
+ * @returns {Promise<void>}
181
+ */
182
+ async backupExisting(cwd) {
183
+ const projectMdPath = join(cwd, '.morph', 'project.md');
184
+ const configJsonPath = join(cwd, '.morph', 'config', 'config.json');
185
+
186
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
187
+
188
+ // Backup project.md if exists
189
+ try {
190
+ await access(projectMdPath);
191
+ const backupPath = join(cwd, '.morph', `project.md.${timestamp}.backup`);
192
+ await copyFile(projectMdPath, backupPath);
193
+ } catch (error) {
194
+ // File doesn't exist, no need to backup
195
+ }
196
+
197
+ // Backup config.json if exists
198
+ try {
199
+ await access(configJsonPath);
200
+ const backupPath = join(cwd, '.morph', 'config', `config.json.${timestamp}.backup`);
201
+ await copyFile(configJsonPath, backupPath);
202
+ } catch (error) {
203
+ // File doesn't exist, no need to backup
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "$schema": "../schema/agent-schema.json",
3
+ "name": "{{name}}",
4
+ "type": "{{type}}",
5
+ "description": "{{description}}",
6
+ "stack": {
7
+ {{#if stack.frontend}}
8
+ "frontend": {
9
+ "tech": "{{stack.frontend.tech}}",
10
+ "version": "{{stack.frontend.version}}"{{#if stack.frontend.details}},
11
+ "details": "{{stack.frontend.details}}"{{/if}}
12
+ },
13
+ {{/if}}
14
+ "backend": {
15
+ "tech": "{{stack.backend.tech}}",
16
+ "version": "{{stack.backend.version}}"{{#if stack.backend.details}},
17
+ "details": "{{stack.backend.details}}"{{/if}}
18
+ }{{#if stack.database}},
19
+ "database": {
20
+ "tech": "{{stack.database.tech}}",
21
+ "version": "{{stack.database.version}}"{{#if stack.database.details}},
22
+ "details": "{{stack.database.details}}"{{/if}}
23
+ }{{/if}}{{#if stack.hosting}},
24
+ "hosting": "{{stack.hosting}}"{{/if}}
25
+ },
26
+ "architecture": "{{architecture}}",
27
+ "conventions": "{{conventions}}",
28
+ "infrastructure": {
29
+ "azure": {{hasAzure}},
30
+ "docker": {{hasDocker}},
31
+ "devops": {{hasDevOps}}
32
+ }{{#if repository}},
33
+ "repository": "{{repository}}"{{/if}},
34
+ "meta": {
35
+ "generatedBy": "morph-spec-cli",
36
+ "generatedAt": "{{generatedAt}}",
37
+ "llmConfidence": {{confidence}},
38
+ "autoDetected": true
39
+ }
40
+ }
@@ -0,0 +1,67 @@
1
+ # {{name}}
2
+
3
+ > {{description}}
4
+
5
+ ## Stack
6
+
7
+ | Component | Technology |
8
+ |-----------|------------|
9
+ {{#if stack.frontend}}
10
+ | **Frontend** | {{stack.frontend.tech}} {{stack.frontend.version}}{{#if stack.frontend.details}} - {{stack.frontend.details}}{{/if}} |
11
+ {{/if}}
12
+ | **Backend** | {{stack.backend.tech}} {{stack.backend.version}}{{#if stack.backend.details}} - {{stack.backend.details}}{{/if}} |
13
+ {{#if stack.database}}
14
+ | **Database** | {{stack.database.tech}} {{stack.database.version}}{{#if stack.database.details}} - {{stack.database.details}}{{/if}} |
15
+ {{/if}}
16
+ {{#if stack.hosting}}
17
+ | **Hosting** | {{stack.hosting}} |
18
+ {{/if}}
19
+
20
+ ## Architecture
21
+
22
+ **Pattern:** {{architecture}}
23
+
24
+ {{projectStructure}}
25
+
26
+ ## Code Conventions
27
+
28
+ {{conventions}}
29
+
30
+ ## Infrastructure
31
+
32
+ {{#if hasAzure}}
33
+ - โœ… **Azure** - Uses Azure infrastructure (Bicep files detected)
34
+ {{else}}
35
+ - โŒ **Azure** - No Azure resources detected
36
+ {{/if}}
37
+
38
+ {{#if hasDocker}}
39
+ - โœ… **Docker** - Containerized (Dockerfile/docker-compose.yml detected)
40
+ {{else}}
41
+ - โŒ **Docker** - Not containerized
42
+ {{/if}}
43
+
44
+ {{#if hasDevOps}}
45
+ - โœ… **CI/CD** - Pipelines detected
46
+ {{else}}
47
+ - โŒ **CI/CD** - No pipelines detected
48
+ {{/if}}
49
+
50
+ {{#if repository}}
51
+ ## Repository
52
+
53
+ {{repository}}
54
+ {{/if}}
55
+
56
+ {{#if warnings.length}}
57
+ ## โš ๏ธ Warnings
58
+
59
+ {{#each warnings}}
60
+ - {{this}}
61
+ {{/each}}
62
+ {{/if}}
63
+
64
+ ---
65
+
66
+ *Auto-generated by MORPH-SPEC CLI on {{generatedAt}}*
67
+ *LLM Confidence: {{confidence}}%*
@@ -0,0 +1,258 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { execSync } from 'child_process';
4
+
5
+ /**
6
+ * Checkpoint Hooks - Automated validation orchestration
7
+ *
8
+ * Runs validators, tests, and linters at checkpoint milestones (every N tasks).
9
+ * Blocks progress if critical validations fail.
10
+ */
11
+
12
+ /**
13
+ * Load LLM interaction configuration
14
+ * @returns {Object} Configuration object
15
+ */
16
+ function loadLLMInteractionConfig() {
17
+ const configPath = join(process.cwd(), '.morph/config/llm-interaction.json');
18
+
19
+ if (!existsSync(configPath)) {
20
+ // Return defaults if config doesn't exist
21
+ return {
22
+ checkpoints: {
23
+ frequency: 3,
24
+ autoValidate: true,
25
+ validators: {
26
+ enabled: ['architecture', 'packages', 'design-system', 'security'],
27
+ optional: []
28
+ },
29
+ hooks: {
30
+ runTests: false,
31
+ runLinters: true,
32
+ buildCheck: false
33
+ },
34
+ onFailure: {
35
+ blockProgress: true,
36
+ maxRetries: 3,
37
+ escalateAfter: 3
38
+ }
39
+ }
40
+ };
41
+ }
42
+
43
+ return JSON.parse(readFileSync(configPath, 'utf8'));
44
+ }
45
+
46
+ /**
47
+ * Run a single validator
48
+ * @param {string} validatorName - Validator to run (architecture, packages, etc.)
49
+ * @param {string} featureName - Feature being validated
50
+ * @returns {Promise<Object>} Validation result
51
+ */
52
+ async function runValidator(validatorName, featureName) {
53
+ try {
54
+ // Use existing validate command
55
+ const result = execSync(
56
+ `node bin/validate.js ${validatorName} --feature=${featureName} --json`,
57
+ { encoding: 'utf8', stdio: 'pipe' }
58
+ );
59
+
60
+ const parsed = JSON.parse(result);
61
+ return {
62
+ validator: validatorName,
63
+ passed: parsed.errors === 0,
64
+ errors: parsed.errors || 0,
65
+ warnings: parsed.warnings || 0,
66
+ details: parsed.issues || []
67
+ };
68
+ } catch (error) {
69
+ return {
70
+ validator: validatorName,
71
+ passed: false,
72
+ errors: 1,
73
+ warnings: 0,
74
+ details: [{ message: error.message, severity: 'error' }]
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Run tests if configured
81
+ * @param {string} featureName - Feature being tested
82
+ * @returns {Promise<Object>} Test result
83
+ */
84
+ async function runTests(featureName) {
85
+ try {
86
+ execSync('npm test --passWithNoTests', { encoding: 'utf8', stdio: 'pipe' });
87
+ return {
88
+ validator: 'tests',
89
+ passed: true,
90
+ errors: 0,
91
+ warnings: 0,
92
+ details: []
93
+ };
94
+ } catch (error) {
95
+ return {
96
+ validator: 'tests',
97
+ passed: false,
98
+ errors: 1,
99
+ warnings: 0,
100
+ details: [{ message: 'Test suite failed', severity: 'error' }]
101
+ };
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Run linters if configured
107
+ * @param {string} featureName - Feature being linted
108
+ * @returns {Promise<Object>} Linter result
109
+ */
110
+ async function runLinters(featureName) {
111
+ try {
112
+ // Check if eslint exists
113
+ if (existsSync(join(process.cwd(), 'node_modules/.bin/eslint'))) {
114
+ execSync('npm run lint --if-present', { encoding: 'utf8', stdio: 'pipe' });
115
+ }
116
+
117
+ return {
118
+ validator: 'linters',
119
+ passed: true,
120
+ errors: 0,
121
+ warnings: 0,
122
+ details: []
123
+ };
124
+ } catch (error) {
125
+ return {
126
+ validator: 'linters',
127
+ passed: false,
128
+ errors: 0,
129
+ warnings: 1,
130
+ details: [{ message: 'Linting warnings detected', severity: 'warning' }]
131
+ };
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Run checkpoint hooks - orchestrates all validation
137
+ * @param {string} featureName - Feature name
138
+ * @param {number} checkpointNum - Checkpoint number (1, 2, 3, etc.)
139
+ * @returns {Promise<Object>} Checkpoint result
140
+ */
141
+ export async function runCheckpointHooks(featureName, checkpointNum) {
142
+ const config = loadLLMInteractionConfig();
143
+ const checkpointConfig = config.checkpoints;
144
+
145
+ if (!checkpointConfig.autoValidate) {
146
+ return {
147
+ passed: true,
148
+ skipped: true,
149
+ message: 'Auto-validation disabled in config'
150
+ };
151
+ }
152
+
153
+ console.log(`\n๐Ÿ” Running CHECKPOINT ${checkpointNum} for feature: ${featureName}`);
154
+ console.log('โ”'.repeat(60));
155
+
156
+ const results = [];
157
+
158
+ // 1. Run enabled validators
159
+ console.log('\n๐Ÿ“‹ Running validators...');
160
+ for (const validatorName of checkpointConfig.validators.enabled) {
161
+ process.stdout.write(` โ€ข ${validatorName}... `);
162
+ const result = await runValidator(validatorName, featureName);
163
+ results.push(result);
164
+
165
+ if (result.passed) {
166
+ console.log('โœ… PASSED');
167
+ } else {
168
+ console.log(`โŒ FAILED (${result.errors} errors, ${result.warnings} warnings)`);
169
+ }
170
+ }
171
+
172
+ // 2. Run tests (if configured)
173
+ if (checkpointConfig.hooks.runTests) {
174
+ console.log('\n๐Ÿงช Running tests...');
175
+ process.stdout.write(' โ€ข test suite... ');
176
+ const testResult = await runTests(featureName);
177
+ results.push(testResult);
178
+
179
+ if (testResult.passed) {
180
+ console.log('โœ… PASSED');
181
+ } else {
182
+ console.log('โŒ FAILED');
183
+ }
184
+ }
185
+
186
+ // 3. Run linters (if configured)
187
+ if (checkpointConfig.hooks.runLinters) {
188
+ console.log('\n๐ŸŽจ Running linters...');
189
+ process.stdout.write(' โ€ข code style... ');
190
+ const lintResult = await runLinters(featureName);
191
+ results.push(lintResult);
192
+
193
+ if (lintResult.passed) {
194
+ console.log('โœ… PASSED');
195
+ } else {
196
+ console.log('โš ๏ธ WARNINGS');
197
+ }
198
+ }
199
+
200
+ // Calculate overall pass/fail
201
+ const errorCount = results.reduce((sum, r) => sum + r.errors, 0);
202
+ const warningCount = results.reduce((sum, r) => sum + r.warnings, 0);
203
+ const passed = errorCount === 0;
204
+
205
+ console.log('\n' + 'โ”'.repeat(60));
206
+ console.log(`\n๐Ÿ“Š Checkpoint ${checkpointNum} Summary:`);
207
+ console.log(` Errors: ${errorCount}`);
208
+ console.log(` Warnings: ${warningCount}`);
209
+ console.log(` Status: ${passed ? 'โœ… PASSED' : 'โŒ FAILED'}`);
210
+
211
+ if (!passed) {
212
+ console.log('\nโš ๏ธ Checkpoint failed! Fix violations before proceeding.');
213
+
214
+ // Show details of failures
215
+ results.filter(r => r.errors > 0).forEach(r => {
216
+ console.log(`\nโŒ ${r.validator} errors:`);
217
+ r.details.forEach(d => {
218
+ if (d.severity === 'error') {
219
+ console.log(` - ${d.message}`);
220
+ }
221
+ });
222
+ });
223
+ }
224
+
225
+ console.log('\n' + 'โ”'.repeat(60) + '\n');
226
+
227
+ return {
228
+ passed,
229
+ checkpointNum,
230
+ timestamp: new Date().toISOString(),
231
+ results,
232
+ summary: {
233
+ errors: errorCount,
234
+ warnings: warningCount,
235
+ validatorsRun: results.length
236
+ }
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Check if checkpoint should run based on task count
242
+ * @param {number} tasksCompleted - Number of tasks completed
243
+ * @param {number} frequency - Checkpoint frequency (default: 3)
244
+ * @returns {boolean} True if checkpoint should run
245
+ */
246
+ export function shouldRunCheckpoint(tasksCompleted, frequency = 3) {
247
+ return tasksCompleted > 0 && tasksCompleted % frequency === 0;
248
+ }
249
+
250
+ /**
251
+ * Get checkpoint number from task count
252
+ * @param {number} tasksCompleted - Number of tasks completed
253
+ * @param {number} frequency - Checkpoint frequency (default: 3)
254
+ * @returns {number} Checkpoint number
255
+ */
256
+ export function getCheckpointNumber(tasksCompleted, frequency = 3) {
257
+ return Math.floor(tasksCompleted / frequency);
258
+ }