@polymorphism-tech/morph-spec 1.0.4 → 2.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.
- package/CLAUDE.md +1381 -0
- package/LICENSE +72 -0
- package/README.md +89 -6
- package/bin/detect-agents.js +225 -0
- package/bin/morph-spec.js +120 -0
- package/bin/render-template.js +302 -0
- package/bin/semantic-detect-agents.js +246 -0
- package/bin/validate-agents-skills.js +239 -0
- package/bin/validate-agents.js +69 -0
- package/bin/validate-phase.js +263 -0
- package/content/.azure/README.md +293 -0
- package/content/.azure/docs/azure-devops-setup.md +454 -0
- package/content/.azure/docs/branch-strategy.md +398 -0
- package/content/.azure/docs/local-development.md +515 -0
- package/content/.azure/pipelines/pipeline-variables.yml +34 -0
- package/content/.azure/pipelines/prod-pipeline.yml +319 -0
- package/content/.azure/pipelines/staging-pipeline.yml +234 -0
- package/content/.azure/pipelines/templates/build-dotnet.yml +75 -0
- package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -0
- package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -0
- package/content/.azure/pipelines/templates/infra-deploy.yml +90 -0
- package/content/.claude/commands/morph-apply.md +118 -26
- package/content/.claude/commands/morph-archive.md +9 -9
- package/content/.claude/commands/morph-clarify.md +184 -0
- package/content/.claude/commands/morph-design.md +275 -0
- package/content/.claude/commands/morph-proposal.md +56 -15
- package/content/.claude/commands/morph-setup.md +100 -0
- package/content/.claude/commands/morph-status.md +47 -32
- package/content/.claude/commands/morph-tasks.md +319 -0
- package/content/.claude/commands/morph-uiux.md +211 -0
- package/content/.claude/skills/specialists/ai-system-architect.md +604 -0
- package/content/.claude/skills/specialists/ms-agent-expert.md +143 -89
- package/content/.claude/skills/specialists/ui-ux-designer.md +744 -9
- package/content/.claude/skills/stacks/dotnet-blazor.md +244 -8
- package/content/.claude/skills/stacks/dotnet-nextjs.md +2 -2
- package/content/.morph/.morphversion +5 -0
- package/content/.morph/config/agents.json +101 -8
- package/content/.morph/config/azure-pricing.json +70 -0
- package/content/.morph/config/azure-pricing.schema.json +50 -0
- package/content/.morph/config/config.template.json +15 -3
- package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -0
- package/content/.morph/hooks/README.md +239 -0
- package/content/.morph/hooks/pre-commit-agents.sh +24 -0
- package/content/.morph/hooks/pre-commit-all.sh +48 -0
- package/content/.morph/hooks/pre-commit-costs.sh +91 -0
- package/content/.morph/hooks/pre-commit-specs.sh +49 -0
- package/content/.morph/hooks/pre-commit-tests.sh +60 -0
- package/content/.morph/project.md +5 -4
- package/content/.morph/schemas/agent.schema.json +296 -0
- package/content/.morph/standards/agent-framework-setup.md +453 -0
- package/content/.morph/standards/architecture.md +142 -7
- package/content/.morph/standards/azure.md +218 -23
- package/content/.morph/standards/coding.md +47 -12
- package/content/.morph/standards/dotnet10-migration.md +494 -0
- package/content/.morph/standards/fluent-ui-setup.md +590 -0
- package/content/.morph/standards/migration-guide.md +514 -0
- package/content/.morph/standards/passkeys-auth.md +423 -0
- package/content/.morph/standards/vector-search-rag.md +536 -0
- package/content/.morph/state.json +18 -0
- package/content/.morph/templates/FluentDesignTheme.cs +149 -0
- package/content/.morph/templates/MudTheme.cs +281 -0
- package/content/.morph/templates/contracts.cs +55 -55
- package/content/.morph/templates/decisions.md +4 -4
- package/content/.morph/templates/design-system.css +226 -0
- package/content/.morph/templates/infra/.dockerignore.example +89 -0
- package/content/.morph/templates/infra/Dockerfile.example +82 -0
- package/content/.morph/templates/infra/README.md +286 -0
- package/content/.morph/templates/infra/app-service.bicep +164 -0
- package/content/.morph/templates/infra/deploy.ps1 +229 -0
- package/content/.morph/templates/infra/deploy.sh +208 -0
- package/content/.morph/templates/infra/main.bicep +41 -7
- package/content/.morph/templates/infra/parameters.dev.json +6 -0
- package/content/.morph/templates/infra/parameters.prod.json +6 -0
- package/content/.morph/templates/infra/parameters.staging.json +29 -0
- package/content/.morph/templates/proposal.md +3 -3
- package/content/.morph/templates/recap.md +3 -3
- package/content/.morph/templates/spec.md +9 -8
- package/content/.morph/templates/sprint-status.yaml +68 -0
- package/content/.morph/templates/state.template.json +222 -0
- package/content/.morph/templates/story.md +143 -0
- package/content/.morph/templates/tasks.md +1 -1
- package/content/.morph/templates/ui-components.md +276 -0
- package/content/.morph/templates/ui-design-system.md +286 -0
- package/content/.morph/templates/ui-flows.md +336 -0
- package/content/.morph/templates/ui-mockups.md +133 -0
- package/content/.morph/test-infra/example.bicep +59 -0
- package/content/CLAUDE.md +124 -0
- package/content/README.md +79 -0
- package/detectors/config-detector.js +223 -0
- package/detectors/conversation-analyzer.js +163 -0
- package/detectors/index.js +84 -0
- package/detectors/standards-generator.js +275 -0
- package/detectors/structure-detector.js +221 -0
- package/docs/README.md +149 -0
- package/docs/api/cost-calculator.js.html +513 -0
- package/docs/api/design-system-generator.js.html +382 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/api/global.html +5263 -0
- package/docs/api/index.html +96 -0
- package/docs/api/scripts/collapse.js +39 -0
- package/docs/api/scripts/commonNav.js +28 -0
- package/docs/api/scripts/linenumber.js +25 -0
- package/docs/api/scripts/nav.js +12 -0
- package/docs/api/scripts/polyfill.js +4 -0
- package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/api/scripts/prettify/lang-css.js +2 -0
- package/docs/api/scripts/prettify/prettify.js +28 -0
- package/docs/api/scripts/search.js +99 -0
- package/docs/api/state-manager.js.html +423 -0
- package/docs/api/styles/jsdoc.css +776 -0
- package/docs/api/styles/prettify.css +80 -0
- package/docs/examples.md +328 -0
- package/docs/getting-started.md +302 -0
- package/docs/installation.md +361 -0
- package/docs/templates.md +418 -0
- package/docs/validation-checklist.md +266 -0
- package/package.json +39 -12
- package/src/commands/cost.js +181 -0
- package/src/commands/create-story.js +283 -0
- package/src/commands/detect.js +104 -0
- package/src/commands/doctor.js +67 -0
- package/src/commands/generate.js +149 -0
- package/src/commands/init.js +69 -45
- package/src/commands/shard-spec.js +224 -0
- package/src/commands/sprint-status.js +250 -0
- package/src/commands/state.js +333 -0
- package/src/commands/sync.js +167 -0
- package/src/commands/update-pricing.js +206 -0
- package/src/commands/update.js +88 -13
- package/src/lib/complexity-analyzer.js +292 -0
- package/src/lib/cost-calculator.js +429 -0
- package/src/lib/design-system-generator.js +298 -0
- package/src/lib/state-manager.js +340 -0
- package/src/utils/file-copier.js +59 -0
- package/src/utils/version-checker.js +175 -0
package/package.json
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polymorphism-tech/morph-spec",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MORPH-SPEC:
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MORPH-SPEC v2.0: AI-First development framework with .NET 10, Microsoft Agent Framework, and Fluent UI Blazor",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"claude",
|
|
8
8
|
"ai-coding",
|
|
9
|
+
"ai-first",
|
|
9
10
|
"spec-driven",
|
|
10
11
|
"dotnet",
|
|
12
|
+
"dotnet10",
|
|
11
13
|
"blazor",
|
|
14
|
+
"agent-framework",
|
|
15
|
+
"fluent-ui",
|
|
12
16
|
"framework",
|
|
13
17
|
"developer-tools",
|
|
14
18
|
"morph",
|
|
15
|
-
"polymorphism"
|
|
19
|
+
"polymorphism",
|
|
20
|
+
"micro-saas"
|
|
16
21
|
],
|
|
17
22
|
"main": "src/index.js",
|
|
18
23
|
"bin": {
|
|
19
|
-
"morph-spec": "
|
|
24
|
+
"morph-spec": "bin/morph-spec.js"
|
|
20
25
|
},
|
|
21
26
|
"files": [
|
|
22
27
|
"bin/",
|
|
23
28
|
"src/",
|
|
29
|
+
"detectors/",
|
|
24
30
|
"content/",
|
|
25
|
-
"
|
|
31
|
+
"docs/",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"CLAUDE.md"
|
|
26
35
|
],
|
|
27
36
|
"type": "module",
|
|
28
37
|
"engines": {
|
|
@@ -30,22 +39,40 @@
|
|
|
30
39
|
},
|
|
31
40
|
"scripts": {
|
|
32
41
|
"start": "node bin/morph-spec.js",
|
|
33
|
-
"test": "node --test"
|
|
42
|
+
"test": "node --test",
|
|
43
|
+
"test:coverage": "c8 --reporter=text --reporter=html --reporter=lcov node --test",
|
|
44
|
+
"test:coverage:summary": "c8 --reporter=text-summary node --test",
|
|
45
|
+
"docs": "jsdoc -c jsdoc.json",
|
|
46
|
+
"docs:watch": "jsdoc -c jsdoc.json --watch",
|
|
47
|
+
"docs:serve": "npx http-server docs/api -p 8080 -o"
|
|
34
48
|
},
|
|
35
49
|
"dependencies": {
|
|
50
|
+
"ajv": "^8.12.0",
|
|
51
|
+
"ajv-formats": "^3.0.1",
|
|
36
52
|
"chalk": "^5.3.0",
|
|
37
53
|
"commander": "^12.0.0",
|
|
38
54
|
"fs-extra": "^11.2.0",
|
|
39
|
-
"
|
|
55
|
+
"glob": "^10.3.0",
|
|
56
|
+
"ora": "^8.0.0",
|
|
57
|
+
"yaml": "^2.3.4"
|
|
40
58
|
},
|
|
41
59
|
"repository": {
|
|
42
60
|
"type": "git",
|
|
43
|
-
"url": "https://github.com/
|
|
61
|
+
"url": "git+https://github.com/lucasPolymorphism/morph-spec-framework.git"
|
|
44
62
|
},
|
|
45
|
-
"author": "Polymorphism Tech",
|
|
46
|
-
"license": "
|
|
47
|
-
"homepage": "https://
|
|
63
|
+
"author": "Polymorphism Tech <contact@polymorphism.com.br>",
|
|
64
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
65
|
+
"homepage": "https://polymorphism.com.br/morph-spec",
|
|
48
66
|
"bugs": {
|
|
49
|
-
"
|
|
67
|
+
"email": "support@polymorphism.com.br"
|
|
68
|
+
},
|
|
69
|
+
"private": false,
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"c8": "^10.1.3",
|
|
75
|
+
"docdash": "^2.0.2",
|
|
76
|
+
"jsdoc": "^4.0.5"
|
|
50
77
|
}
|
|
51
78
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Cost Command
|
|
3
|
+
* CLI wrapper for cost calculation operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
import * as CostCalculator from '../lib/cost-calculator.js';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Command Function
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Calculate Azure infrastructure costs from Bicep files
|
|
17
|
+
* @param {string} bicepFiles - File path or glob pattern
|
|
18
|
+
* @param {Object} options - CLI options
|
|
19
|
+
*/
|
|
20
|
+
export async function costCommand(bicepFiles, options) {
|
|
21
|
+
if (!bicepFiles) {
|
|
22
|
+
logger.error('Bicep file path required');
|
|
23
|
+
logger.dim(' Usage: morph-spec cost <bicep-files>');
|
|
24
|
+
logger.blank();
|
|
25
|
+
logger.dim(' Examples:');
|
|
26
|
+
logger.dim(' morph-spec cost infra/main.bicep');
|
|
27
|
+
logger.dim(' morph-spec cost "infra/**/*.bicep"');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
logger.header('MORPH-SPEC Cost Calculator');
|
|
32
|
+
logger.blank();
|
|
33
|
+
|
|
34
|
+
const spinner = ora('Parsing Bicep files...').start();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Calculate costs
|
|
38
|
+
const result = CostCalculator.calculateBicepCost(bicepFiles, {
|
|
39
|
+
configPath: options.config
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
spinner.succeed(`Found ${result.breakdown.length} resources`);
|
|
43
|
+
logger.blank();
|
|
44
|
+
|
|
45
|
+
// JSON output
|
|
46
|
+
if (options.json) {
|
|
47
|
+
console.log(JSON.stringify(result, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Verbose output (table format)
|
|
52
|
+
if (options.verbose) {
|
|
53
|
+
displayVerbose(result);
|
|
54
|
+
} else {
|
|
55
|
+
displaySummary(result);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Exit code based on cost thresholds
|
|
59
|
+
if (result.requiresADR && options.strict) {
|
|
60
|
+
logger.blank();
|
|
61
|
+
logger.error('Cost exceeds ADR threshold!');
|
|
62
|
+
logger.dim(' Add ADR in decisions.md or use --no-strict');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
spinner.fail('Cost calculation failed');
|
|
68
|
+
logger.error(error.message);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Display Functions
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Display summary (default output)
|
|
79
|
+
*/
|
|
80
|
+
function displaySummary(result) {
|
|
81
|
+
logger.header('Cost Summary');
|
|
82
|
+
logger.blank();
|
|
83
|
+
|
|
84
|
+
// Total cost
|
|
85
|
+
const costColor = result.requiresADR ? 'red' : result.requiresApproval ? 'yellow' : 'green';
|
|
86
|
+
logger.info(`Monthly Cost: ${chalk[costColor](`$${result.monthly.toFixed(2)}`)}`);
|
|
87
|
+
logger.dim(` Currency: ${result.currency}`);
|
|
88
|
+
logger.dim(` Region: ${result.region}`);
|
|
89
|
+
logger.blank();
|
|
90
|
+
|
|
91
|
+
// Status
|
|
92
|
+
if (result.isFreeTier) {
|
|
93
|
+
logger.success('✓ All resources use free tier');
|
|
94
|
+
} else {
|
|
95
|
+
if (result.requiresADR) {
|
|
96
|
+
logger.warn('⚠️ Requires ADR in decisions.md (cost > $' + result.limits.requiresADR + ')');
|
|
97
|
+
} else if (result.requiresApproval) {
|
|
98
|
+
logger.warn('⚠️ Requires approval (cost > $' + result.limits.freeTierOnly + ')');
|
|
99
|
+
} else {
|
|
100
|
+
logger.success('✓ Within free tier limits');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
logger.blank();
|
|
104
|
+
|
|
105
|
+
// Top 3 expensive resources
|
|
106
|
+
if (result.breakdown.length > 0) {
|
|
107
|
+
logger.header('Top Resources:');
|
|
108
|
+
const sorted = [...result.breakdown].sort((a, b) => b.cost - a.cost);
|
|
109
|
+
sorted.slice(0, 3).forEach(item => {
|
|
110
|
+
const costStr = chalk.cyan(`$${item.cost.toFixed(2)}/mo`);
|
|
111
|
+
logger.dim(` - ${item.name} (${item.sku}): ${costStr}`);
|
|
112
|
+
});
|
|
113
|
+
logger.blank();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Warnings
|
|
117
|
+
if (result.warnings.length > 0) {
|
|
118
|
+
logger.header('Warnings:');
|
|
119
|
+
result.warnings.forEach(warning => {
|
|
120
|
+
logger.warn(` ${warning}`);
|
|
121
|
+
});
|
|
122
|
+
logger.blank();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Next steps
|
|
126
|
+
if (result.requiresADR) {
|
|
127
|
+
logger.header('Next Steps:');
|
|
128
|
+
logger.dim(' 1. Document cost justification in decisions.md');
|
|
129
|
+
logger.dim(' 2. Include ADR with cost breakdown');
|
|
130
|
+
logger.dim(' 3. Get approval before deploying');
|
|
131
|
+
logger.blank();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Display verbose (table format)
|
|
137
|
+
*/
|
|
138
|
+
function displayVerbose(result) {
|
|
139
|
+
logger.blank();
|
|
140
|
+
console.log('╔════════════════════════════════════════════════════════════════╗');
|
|
141
|
+
console.log('║ MORPH-SPEC COST CALCULATOR ║');
|
|
142
|
+
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
143
|
+
console.log(`║ Region: ${result.region.padEnd(54)}║`);
|
|
144
|
+
console.log(`║ Currency: ${result.currency.padEnd(52)}║`);
|
|
145
|
+
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
146
|
+
console.log('║ RESOURCES ║');
|
|
147
|
+
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
148
|
+
|
|
149
|
+
for (const item of result.breakdown) {
|
|
150
|
+
const costStr = `$${item.cost.toFixed(2)}/mo`;
|
|
151
|
+
const nameStr = `${item.name} (${item.sku})`;
|
|
152
|
+
console.log(`║ ${nameStr.substring(0, 40).padEnd(40)} ${costStr.padStart(20)}║`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
156
|
+
console.log('║ SUMMARY ║');
|
|
157
|
+
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
158
|
+
console.log(`║ Total Monthly Cost: $${result.monthly.toFixed(2).padStart(42)}║`);
|
|
159
|
+
console.log(`║ Requires Approval: ${(result.requiresApproval ? 'YES' : 'NO').padStart(43)}║`);
|
|
160
|
+
console.log(`║ Requires ADR: ${(result.requiresADR ? 'YES' : 'NO').padStart(48)}║`);
|
|
161
|
+
|
|
162
|
+
if (result.warnings.length > 0) {
|
|
163
|
+
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
164
|
+
console.log('║ WARNINGS ║');
|
|
165
|
+
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
166
|
+
for (const warning of result.warnings) {
|
|
167
|
+
const chunks = warning.match(/.{1,62}/g) || [warning];
|
|
168
|
+
chunks.forEach(chunk => {
|
|
169
|
+
console.log(`║ ${chunk.padEnd(62)}║`);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
175
|
+
console.log('║ LIMITS (from config) ║');
|
|
176
|
+
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
177
|
+
console.log(`║ Free Tier Only: $${result.limits.freeTierOnly.toFixed(2).padStart(44)}║`);
|
|
178
|
+
console.log(`║ With Approval: $${result.limits.withApproval.toFixed(2).padStart(45)}║`);
|
|
179
|
+
console.log(`║ Requires ADR: $${result.limits.requiresADR.toFixed(2).padStart(46)}║`);
|
|
180
|
+
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
181
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Story Creator
|
|
3
|
+
* Creates self-contained story files with auto-injected Dev Notes
|
|
4
|
+
* Inspired by BMAD Method story-driven development
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { logger } from '../utils/logger.js';
|
|
12
|
+
import { ensureDir, writeFile } from '../utils/file-copier.js';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Helper Functions
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
function readTemplate(templateName) {
|
|
19
|
+
const templatePath = path.join(process.cwd(), 'node_modules/@polymorphism-tech/morph-spec/content/.morph/templates', templateName);
|
|
20
|
+
|
|
21
|
+
// Fallback to local if in development
|
|
22
|
+
if (!fs.existsSync(templatePath)) {
|
|
23
|
+
const localPath = path.join(process.cwd(), `content/.morph/templates/${templateName}`);
|
|
24
|
+
if (fs.existsSync(localPath)) {
|
|
25
|
+
return fs.readFileSync(localPath, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return fs.readFileSync(templatePath, 'utf-8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readSpec(featureName) {
|
|
34
|
+
const specPath = path.join(process.cwd(), `.morph/project/outputs/${featureName}/spec.md`);
|
|
35
|
+
const shardedIndexPath = path.join(process.cwd(), `.morph/project/outputs/${featureName}/spec/index.md`);
|
|
36
|
+
|
|
37
|
+
// Try sharded spec first (BMAD pattern)
|
|
38
|
+
if (fs.existsSync(shardedIndexPath)) {
|
|
39
|
+
return { isSharded: true, indexPath: shardedIndexPath };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fallback to monolithic spec
|
|
43
|
+
if (fs.existsSync(specPath)) {
|
|
44
|
+
return { isSharded: false, content: fs.readFileSync(specPath, 'utf-8') };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw new Error(`Spec not found for feature: ${featureName}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extractContextFromSpec(specContent, sectionHeading) {
|
|
51
|
+
// Extract section content between ## heading and next ##
|
|
52
|
+
const regex = new RegExp(`## ${sectionHeading}\\s*([\\s\\S]*?)(?=\\n## |$)`, 'i');
|
|
53
|
+
const match = specContent.match(regex);
|
|
54
|
+
return match ? match[1].trim() : 'See spec.md for complete context';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function detectPatterns(tasks, specContent) {
|
|
58
|
+
const patterns = {
|
|
59
|
+
hasEntity: tasks.some(t => /entity|model|domain/i.test(t)),
|
|
60
|
+
hasService: tasks.some(t => /service|business|logic/i.test(t)),
|
|
61
|
+
hasComponent: tasks.some(t => /component|razor|blazor|ui/i.test(t)),
|
|
62
|
+
hasRepository: tasks.some(t => /repository|data|persistence/i.test(t)),
|
|
63
|
+
hasJob: tasks.some(t => /job|background|hangfire|scheduled/i.test(t)),
|
|
64
|
+
hasAI: tasks.some(t => /agent|ai|llm|semantic|rag/i.test(t)),
|
|
65
|
+
hasAzure: tasks.some(t => /bicep|azure|infra|deploy/i.test(t)),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return patterns;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function injectDevNotes(patterns, storyData) {
|
|
72
|
+
const notes = [];
|
|
73
|
+
|
|
74
|
+
// Entity pattern
|
|
75
|
+
if (patterns.hasEntity) {
|
|
76
|
+
notes.push('Use Primary Constructor (.NET 10 feature)');
|
|
77
|
+
notes.push('Follow entity pattern: .morph/project/standards/coding.md#entity-pattern');
|
|
78
|
+
notes.push('Navigation properties: configure in DbContext with Fluent API');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Service pattern
|
|
82
|
+
if (patterns.hasService) {
|
|
83
|
+
notes.push('Service layer: implement interface-first (IXService → XService)');
|
|
84
|
+
notes.push('Dependency injection: register in Program.cs as Scoped');
|
|
85
|
+
notes.push('Error handling: use Result<T> pattern from standards/architecture.md');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Blazor component
|
|
89
|
+
if (patterns.hasComponent) {
|
|
90
|
+
notes.push('UI Library: Fluent UI Blazor (see .morph/project/standards/ui.md)');
|
|
91
|
+
notes.push('Design System: use CSS variables from wwwroot/css/design-system.css');
|
|
92
|
+
notes.push('State management: @inject for services, [Parameter] for props');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Repository pattern
|
|
96
|
+
if (patterns.hasRepository) {
|
|
97
|
+
notes.push('Repository pattern: IRepository<T> generic base');
|
|
98
|
+
notes.push('EF Core: use AsNoTracking() for read-only queries');
|
|
99
|
+
notes.push('Transactions: wrap in DbContext.Database.BeginTransactionAsync()');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Background job
|
|
103
|
+
if (patterns.hasJob) {
|
|
104
|
+
notes.push('Hangfire: implement IJob interface, register in HangfireConfig.cs');
|
|
105
|
+
notes.push('Retry policy: use [AutomaticRetry(Attempts = 3)]');
|
|
106
|
+
notes.push('Logging: inject ILogger<TJob> for monitoring');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// AI/Agent
|
|
110
|
+
if (patterns.hasAI) {
|
|
111
|
+
notes.push('Microsoft Agent Framework: use AgentBuilder (not Semantic Kernel)');
|
|
112
|
+
notes.push('Prompts: store in /prompts/{agent-name}.md');
|
|
113
|
+
notes.push('Vector search: use EF Core 10 Vector Search (see standards/vector-search-rag.md)');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Azure/IaC
|
|
117
|
+
if (patterns.hasAzure) {
|
|
118
|
+
notes.push('Infrastructure as Code: ALWAYS use Bicep (never portal)');
|
|
119
|
+
notes.push('Cost validation: run morph-spec cost before commit');
|
|
120
|
+
notes.push('Deployment: Azure Container Apps (not App Service)');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Add spec reference
|
|
124
|
+
if (storyData.specShard) {
|
|
125
|
+
notes.push(`Spec reference: .morph/project/outputs/${storyData.featureName}/spec/${storyData.specShard}.md`);
|
|
126
|
+
} else {
|
|
127
|
+
notes.push(`Spec reference: .morph/project/outputs/${storyData.featureName}/spec.md`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return notes;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderStory(template, data) {
|
|
134
|
+
let rendered = template;
|
|
135
|
+
|
|
136
|
+
// Simple placeholder replacement (non-Mustache for simplicity)
|
|
137
|
+
const replacements = {
|
|
138
|
+
'{{STORY_ID}}': data.storyId || 'STORY-XXX',
|
|
139
|
+
'{{STORY_TITLE}}': data.storyTitle || 'Story Title',
|
|
140
|
+
'{{FEATURE_NAME}}': data.featureName || 'feature-name',
|
|
141
|
+
'{{FEATURE_NAME_TITLE}}': data.featureNameTitle || 'Feature Name',
|
|
142
|
+
'{{EPIC_NAME}}': data.epicName || 'Epic Name',
|
|
143
|
+
'{{DATE}}': new Date().toISOString().split('T')[0],
|
|
144
|
+
'{{STORY_CONTEXT}}': data.context || 'Context not provided',
|
|
145
|
+
'{{SPEC_SHARD}}': data.specShard || 'spec',
|
|
146
|
+
'{{EFFORT}}': data.effort || '1 day',
|
|
147
|
+
'{{CODING_PATTERN}}': data.codingPattern || 'general',
|
|
148
|
+
'{{ARCH_PATTERN}}': data.archPattern || 'clean-architecture',
|
|
149
|
+
'{{STATUS}}': 'Ready',
|
|
150
|
+
'{{CREATED_DATE}}': new Date().toISOString().split('T')[0],
|
|
151
|
+
'{{ADDITIONAL_NOTES}}': data.additionalNotes || '',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
155
|
+
rendered = rendered.replaceAll(placeholder, value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle arrays (tasks, dev notes, acceptance criteria)
|
|
159
|
+
if (data.tasks && data.tasks.length > 0) {
|
|
160
|
+
const tasksSection = data.tasks.map(t => `- [ ] ${t}`).join('\n');
|
|
161
|
+
rendered = rendered.replace(/{{#TASKS}}[\s\S]*?{{\/TASKS}}/g, tasksSection);
|
|
162
|
+
} else {
|
|
163
|
+
rendered = rendered.replace(/{{#TASKS}}[\s\S]*?{{\/TASKS}}/g, '- [ ] Task 1\n- [ ] Task 2');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (data.devNotes && data.devNotes.length > 0) {
|
|
167
|
+
const devNotesSection = data.devNotes.map(n => `- ${n}`).join('\n');
|
|
168
|
+
rendered = rendered.replace(/{{#DEV_NOTES}}[\s\S]*?{{\/DEV_NOTES}}/g, devNotesSection);
|
|
169
|
+
} else {
|
|
170
|
+
rendered = rendered.replace(/{{#DEV_NOTES}}[\s\S]*?{{\/DEV_NOTES}}/g, '- Follow standards in .morph/project/standards/');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (data.acceptanceCriteria && data.acceptanceCriteria.length > 0) {
|
|
174
|
+
const acSection = data.acceptanceCriteria.map(ac =>
|
|
175
|
+
`- [ ] **GIVEN** ${ac.given} **WHEN** ${ac.when} **THEN** ${ac.then}`
|
|
176
|
+
).join('\n');
|
|
177
|
+
rendered = rendered.replace(/{{#ACCEPTANCE_CRITERIA}}[\s\S]*?{{\/ACCEPTANCE_CRITERIA}}/g, acSection);
|
|
178
|
+
} else {
|
|
179
|
+
rendered = rendered.replace(/{{#ACCEPTANCE_CRITERIA}}[\s\S]*?{{\/ACCEPTANCE_CRITERIA}}/g,
|
|
180
|
+
'- [ ] **GIVEN** [condition] **WHEN** [action] **THEN** [result]');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Clean up remaining Mustache sections (empty arrays)
|
|
184
|
+
rendered = rendered.replace(/{{#\w+}}[\s\S]*?{{\/\w+}}/g, '');
|
|
185
|
+
|
|
186
|
+
return rendered;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toTitleCase(str) {
|
|
190
|
+
return str
|
|
191
|
+
.split('-')
|
|
192
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
193
|
+
.join(' ');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Command Function
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
export async function createStoryCommand(feature, storyId, options) {
|
|
201
|
+
logger.header('MORPH-SPEC Story Creator');
|
|
202
|
+
logger.dim(`Feature: ${feature}`);
|
|
203
|
+
logger.dim(`Story ID: ${storyId}`);
|
|
204
|
+
logger.blank();
|
|
205
|
+
|
|
206
|
+
const spinner = ora('Creating story...').start();
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Read template
|
|
210
|
+
const template = readTemplate('story.md');
|
|
211
|
+
|
|
212
|
+
// Read spec (detect if sharded)
|
|
213
|
+
const spec = readSpec(feature);
|
|
214
|
+
|
|
215
|
+
// Build story data
|
|
216
|
+
const storyData = {
|
|
217
|
+
featureName: feature,
|
|
218
|
+
featureNameTitle: toTitleCase(feature),
|
|
219
|
+
storyId,
|
|
220
|
+
storyTitle: options.title || 'Story Title',
|
|
221
|
+
epicName: options.epic || toTitleCase(feature),
|
|
222
|
+
context: options.context || (spec.isSharded
|
|
223
|
+
? 'See sharded spec for complete context'
|
|
224
|
+
: extractContextFromSpec(spec.content, 'Overview')),
|
|
225
|
+
specShard: spec.isSharded ? 'index' : null,
|
|
226
|
+
tasks: options.tasks ? options.tasks.split(',').map(t => t.trim()) : ['Task 1', 'Task 2'],
|
|
227
|
+
effort: '1 day',
|
|
228
|
+
additionalNotes: '',
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Detect patterns and inject Dev Notes
|
|
232
|
+
const patterns = detectPatterns(storyData.tasks, spec.content || '');
|
|
233
|
+
storyData.devNotes = injectDevNotes(patterns, storyData);
|
|
234
|
+
|
|
235
|
+
// Default acceptance criteria
|
|
236
|
+
storyData.acceptanceCriteria = [
|
|
237
|
+
{ given: '[condition]', when: '[action]', then: '[expected result]' },
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
// Render story
|
|
241
|
+
const renderedStory = renderStory(template, storyData);
|
|
242
|
+
|
|
243
|
+
// Output
|
|
244
|
+
const outputDir = path.join(process.cwd(), `.morph/project/outputs/${feature}/stories`);
|
|
245
|
+
const outputPath = path.join(outputDir, `${storyId}.md`);
|
|
246
|
+
|
|
247
|
+
if (options.dryRun) {
|
|
248
|
+
spinner.info('Dry run - preview only');
|
|
249
|
+
logger.blank();
|
|
250
|
+
console.log(renderedStory);
|
|
251
|
+
logger.blank();
|
|
252
|
+
logger.dim(`Would be written to: ${outputPath}`);
|
|
253
|
+
} else {
|
|
254
|
+
// Ensure directory exists
|
|
255
|
+
await ensureDir(outputDir);
|
|
256
|
+
|
|
257
|
+
// Write file
|
|
258
|
+
await writeFile(outputPath, renderedStory);
|
|
259
|
+
|
|
260
|
+
spinner.succeed('Story created!');
|
|
261
|
+
logger.blank();
|
|
262
|
+
|
|
263
|
+
logger.success(`Story file: ${chalk.cyan(outputPath)}`);
|
|
264
|
+
logger.blank();
|
|
265
|
+
|
|
266
|
+
logger.header('Dev Notes Auto-Injected:');
|
|
267
|
+
storyData.devNotes.forEach(note => logger.dim(` - ${note}`));
|
|
268
|
+
logger.blank();
|
|
269
|
+
|
|
270
|
+
logger.header('Next Steps:');
|
|
271
|
+
logger.dim(' 1. Review and customize story file');
|
|
272
|
+
logger.dim(' 2. Run in fresh Claude session: /dev → implement story');
|
|
273
|
+
logger.dim(' 3. Dev adds implementation notes');
|
|
274
|
+
logger.dim(' 4. QA reviews and adds QA notes');
|
|
275
|
+
logger.blank();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
} catch (error) {
|
|
279
|
+
spinner.fail('Failed to create story');
|
|
280
|
+
logger.error(error.message);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { detectProject, getDetectionSummary } from '../../detectors/index.js';
|
|
6
|
+
import { ensureDir, writeFile } from '../utils/file-copier.js';
|
|
7
|
+
|
|
8
|
+
export async function detectCommand(options) {
|
|
9
|
+
const targetPath = options.path || process.cwd();
|
|
10
|
+
|
|
11
|
+
logger.header('MORPH-SPEC Project Detection');
|
|
12
|
+
logger.dim(`Analyzing: ${targetPath}`);
|
|
13
|
+
logger.blank();
|
|
14
|
+
|
|
15
|
+
const spinner = ora('Detecting project structure...').start();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Run detection
|
|
19
|
+
const results = await detectProject(targetPath, {
|
|
20
|
+
structure: true,
|
|
21
|
+
config: true,
|
|
22
|
+
conversation: true,
|
|
23
|
+
generateStandards: true
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
spinner.succeed('Detection complete!');
|
|
27
|
+
logger.blank();
|
|
28
|
+
|
|
29
|
+
// Display summary
|
|
30
|
+
logger.header('Detection Results');
|
|
31
|
+
logger.blank();
|
|
32
|
+
|
|
33
|
+
// Stack
|
|
34
|
+
logger.info(`Stack: ${chalk.cyan(results.structure.stack)}`);
|
|
35
|
+
logger.info(`Architecture: ${chalk.cyan(results.structure.architecture)}`);
|
|
36
|
+
if (results.structure.uiLibrary) {
|
|
37
|
+
logger.info(`UI Library: ${chalk.cyan(results.structure.uiLibrary)}`);
|
|
38
|
+
}
|
|
39
|
+
logger.blank();
|
|
40
|
+
|
|
41
|
+
// Technologies
|
|
42
|
+
if (results.config.technologies.length > 0) {
|
|
43
|
+
logger.header('Technologies:');
|
|
44
|
+
results.config.technologies.forEach(tech => {
|
|
45
|
+
logger.dim(` - ${tech}`);
|
|
46
|
+
});
|
|
47
|
+
logger.blank();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Patterns
|
|
51
|
+
if (results.structure.patterns.length > 0) {
|
|
52
|
+
logger.header('Patterns:');
|
|
53
|
+
results.structure.patterns.forEach(pattern => {
|
|
54
|
+
logger.dim(` - ${pattern}`);
|
|
55
|
+
});
|
|
56
|
+
logger.blank();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Recommendations
|
|
60
|
+
if (results.inferred.recommendations.length > 0) {
|
|
61
|
+
logger.header('Recommendations:');
|
|
62
|
+
results.inferred.recommendations.forEach(rec => {
|
|
63
|
+
logger.warn(` ⚠ ${rec}`);
|
|
64
|
+
});
|
|
65
|
+
logger.blank();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Save results if requested
|
|
69
|
+
if (options.save !== false) {
|
|
70
|
+
spinner.start('Saving detection results...');
|
|
71
|
+
|
|
72
|
+
const outputDir = join(targetPath, '.morph', 'project', 'context');
|
|
73
|
+
await ensureDir(outputDir);
|
|
74
|
+
|
|
75
|
+
// Save detection log
|
|
76
|
+
const logPath = join(outputDir, 'detection-log.md');
|
|
77
|
+
const summary = getDetectionSummary(results);
|
|
78
|
+
await writeFile(logPath, summary);
|
|
79
|
+
|
|
80
|
+
// Save inferred standards
|
|
81
|
+
const standardsDir = join(targetPath, '.morph', 'project', 'standards');
|
|
82
|
+
await ensureDir(standardsDir);
|
|
83
|
+
|
|
84
|
+
const standardsPath = join(standardsDir, 'inferred.md');
|
|
85
|
+
await writeFile(standardsPath, results.inferred.markdown);
|
|
86
|
+
|
|
87
|
+
spinner.succeed('Results saved!');
|
|
88
|
+
logger.dim(` - ${logPath}`);
|
|
89
|
+
logger.dim(` - ${standardsPath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Verbose output
|
|
93
|
+
if (options.verbose) {
|
|
94
|
+
logger.blank();
|
|
95
|
+
logger.header('Detailed Results (JSON):');
|
|
96
|
+
console.log(JSON.stringify(results, null, 2));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
spinner.fail('Detection failed');
|
|
101
|
+
logger.error(error.message);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|