@polymorphism-tech/morph-spec 1.0.2 → 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.
Files changed (152) hide show
  1. package/CLAUDE.md +1381 -0
  2. package/LICENSE +72 -0
  3. package/README.md +114 -12
  4. package/bin/detect-agents.js +225 -0
  5. package/bin/morph-spec.js +120 -0
  6. package/bin/render-template.js +302 -0
  7. package/bin/semantic-detect-agents.js +246 -0
  8. package/bin/validate-agents-skills.js +239 -0
  9. package/bin/validate-agents.js +69 -0
  10. package/bin/validate-phase.js +263 -0
  11. package/content/.azure/README.md +293 -0
  12. package/content/.azure/docs/azure-devops-setup.md +454 -0
  13. package/content/.azure/docs/branch-strategy.md +398 -0
  14. package/content/.azure/docs/local-development.md +515 -0
  15. package/content/.azure/pipelines/pipeline-variables.yml +34 -0
  16. package/content/.azure/pipelines/prod-pipeline.yml +319 -0
  17. package/content/.azure/pipelines/staging-pipeline.yml +234 -0
  18. package/content/.azure/pipelines/templates/build-dotnet.yml +75 -0
  19. package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -0
  20. package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -0
  21. package/content/.azure/pipelines/templates/infra-deploy.yml +90 -0
  22. package/content/.claude/commands/morph-apply.md +118 -26
  23. package/content/.claude/commands/morph-archive.md +9 -9
  24. package/content/.claude/commands/morph-clarify.md +184 -0
  25. package/content/.claude/commands/morph-design.md +275 -0
  26. package/content/.claude/commands/morph-proposal.md +56 -15
  27. package/content/.claude/commands/morph-setup.md +100 -0
  28. package/content/.claude/commands/morph-status.md +47 -32
  29. package/content/.claude/commands/morph-tasks.md +319 -0
  30. package/content/.claude/commands/morph-uiux.md +211 -0
  31. package/content/.claude/skills/specialists/ai-system-architect.md +604 -0
  32. package/content/.claude/skills/specialists/ms-agent-expert.md +143 -89
  33. package/content/.claude/skills/specialists/ui-ux-designer.md +744 -9
  34. package/content/.claude/skills/stacks/dotnet-blazor.md +244 -8
  35. package/content/.claude/skills/stacks/dotnet-nextjs.md +2 -2
  36. package/content/.morph/.morphversion +5 -0
  37. package/content/.morph/config/agents.json +101 -8
  38. package/content/.morph/config/azure-pricing.json +70 -0
  39. package/content/.morph/config/azure-pricing.schema.json +50 -0
  40. package/content/.morph/config/config.template.json +15 -3
  41. package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -0
  42. package/content/.morph/hooks/README.md +239 -0
  43. package/content/.morph/hooks/pre-commit-agents.sh +24 -0
  44. package/content/.morph/hooks/pre-commit-all.sh +48 -0
  45. package/content/.morph/hooks/pre-commit-costs.sh +91 -0
  46. package/content/.morph/hooks/pre-commit-specs.sh +49 -0
  47. package/content/.morph/hooks/pre-commit-tests.sh +60 -0
  48. package/content/.morph/project.md +5 -4
  49. package/content/.morph/schemas/agent.schema.json +296 -0
  50. package/content/.morph/standards/agent-framework-setup.md +453 -0
  51. package/content/.morph/standards/architecture.md +142 -7
  52. package/content/.morph/standards/azure.md +218 -23
  53. package/content/.morph/standards/coding.md +47 -12
  54. package/content/.morph/standards/dotnet10-migration.md +494 -0
  55. package/content/.morph/standards/fluent-ui-setup.md +590 -0
  56. package/content/.morph/standards/migration-guide.md +514 -0
  57. package/content/.morph/standards/passkeys-auth.md +423 -0
  58. package/content/.morph/standards/vector-search-rag.md +536 -0
  59. package/content/.morph/state.json +18 -0
  60. package/content/.morph/templates/FluentDesignTheme.cs +149 -0
  61. package/content/.morph/templates/MudTheme.cs +281 -0
  62. package/content/.morph/templates/contracts.cs +55 -55
  63. package/content/.morph/templates/decisions.md +4 -4
  64. package/content/.morph/templates/design-system.css +226 -0
  65. package/content/.morph/templates/infra/.dockerignore.example +89 -0
  66. package/content/.morph/templates/infra/Dockerfile.example +82 -0
  67. package/content/.morph/templates/infra/README.md +286 -0
  68. package/content/.morph/templates/infra/app-service.bicep +164 -0
  69. package/content/.morph/templates/infra/deploy.ps1 +229 -0
  70. package/content/.morph/templates/infra/deploy.sh +208 -0
  71. package/content/.morph/templates/infra/main.bicep +41 -7
  72. package/content/.morph/templates/infra/parameters.dev.json +6 -0
  73. package/content/.morph/templates/infra/parameters.prod.json +6 -0
  74. package/content/.morph/templates/infra/parameters.staging.json +29 -0
  75. package/content/.morph/templates/proposal.md +3 -3
  76. package/content/.morph/templates/recap.md +3 -3
  77. package/content/.morph/templates/spec.md +9 -8
  78. package/content/.morph/templates/sprint-status.yaml +68 -0
  79. package/content/.morph/templates/state.template.json +222 -0
  80. package/content/.morph/templates/story.md +143 -0
  81. package/content/.morph/templates/tasks.md +1 -1
  82. package/content/.morph/templates/ui-components.md +276 -0
  83. package/content/.morph/templates/ui-design-system.md +286 -0
  84. package/content/.morph/templates/ui-flows.md +336 -0
  85. package/content/.morph/templates/ui-mockups.md +133 -0
  86. package/content/.morph/test-infra/example.bicep +59 -0
  87. package/content/CLAUDE.md +124 -0
  88. package/content/README.md +79 -0
  89. package/detectors/config-detector.js +223 -0
  90. package/detectors/conversation-analyzer.js +163 -0
  91. package/detectors/index.js +84 -0
  92. package/detectors/standards-generator.js +275 -0
  93. package/detectors/structure-detector.js +221 -0
  94. package/docs/README.md +149 -0
  95. package/docs/api/cost-calculator.js.html +513 -0
  96. package/docs/api/design-system-generator.js.html +382 -0
  97. package/docs/api/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  98. package/docs/api/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  99. package/docs/api/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  100. package/docs/api/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  101. package/docs/api/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  102. package/docs/api/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  103. package/docs/api/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  104. package/docs/api/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  105. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  106. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  107. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  108. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  109. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  110. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  111. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  112. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  113. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  114. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  115. package/docs/api/global.html +5263 -0
  116. package/docs/api/index.html +96 -0
  117. package/docs/api/scripts/collapse.js +39 -0
  118. package/docs/api/scripts/commonNav.js +28 -0
  119. package/docs/api/scripts/linenumber.js +25 -0
  120. package/docs/api/scripts/nav.js +12 -0
  121. package/docs/api/scripts/polyfill.js +4 -0
  122. package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
  123. package/docs/api/scripts/prettify/lang-css.js +2 -0
  124. package/docs/api/scripts/prettify/prettify.js +28 -0
  125. package/docs/api/scripts/search.js +99 -0
  126. package/docs/api/state-manager.js.html +423 -0
  127. package/docs/api/styles/jsdoc.css +776 -0
  128. package/docs/api/styles/prettify.css +80 -0
  129. package/docs/examples.md +328 -0
  130. package/docs/getting-started.md +302 -0
  131. package/docs/installation.md +361 -0
  132. package/docs/templates.md +418 -0
  133. package/docs/validation-checklist.md +266 -0
  134. package/package.json +39 -12
  135. package/src/commands/cost.js +181 -0
  136. package/src/commands/create-story.js +283 -0
  137. package/src/commands/detect.js +104 -0
  138. package/src/commands/doctor.js +67 -0
  139. package/src/commands/generate.js +149 -0
  140. package/src/commands/init.js +71 -46
  141. package/src/commands/shard-spec.js +224 -0
  142. package/src/commands/sprint-status.js +250 -0
  143. package/src/commands/state.js +333 -0
  144. package/src/commands/sync.js +167 -0
  145. package/src/commands/update-pricing.js +206 -0
  146. package/src/commands/update.js +88 -13
  147. package/src/lib/complexity-analyzer.js +292 -0
  148. package/src/lib/cost-calculator.js +429 -0
  149. package/src/lib/design-system-generator.js +298 -0
  150. package/src/lib/state-manager.js +340 -0
  151. package/src/utils/file-copier.js +63 -0
  152. 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": "1.0.2",
4
- "description": "MORPH-SPEC: Methodical Orchestration for Reliable Production-ready SPEC-driven development framework for Claude Code",
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": "./bin/morph-spec.js"
24
+ "morph-spec": "bin/morph-spec.js"
20
25
  },
21
26
  "files": [
22
27
  "bin/",
23
28
  "src/",
29
+ "detectors/",
24
30
  "content/",
25
- "README.md"
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
- "ora": "^8.0.0"
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/polymorphism-tech/morph-spec-framework.git"
61
+ "url": "git+https://github.com/lucasPolymorphism/morph-spec-framework.git"
44
62
  },
45
- "author": "Polymorphism Tech",
46
- "license": "MIT",
47
- "homepage": "https://github.com/polymorphism-tech/morph-spec-framework#readme",
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
- "url": "https://github.com/polymorphism-tech/morph-spec-framework/issues"
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
+ }