@polymorphism-tech/morph-spec 2.2.0 → 2.4.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 (251) hide show
  1. package/CLAUDE.md +314 -1673
  2. package/LICENSE +72 -72
  3. package/README.md +515 -516
  4. package/bin/detect-agents.js +225 -225
  5. package/bin/morph-spec.js +358 -173
  6. package/bin/render-template.js +302 -302
  7. package/bin/semantic-detect-agents.js +246 -246
  8. package/bin/task-manager.js +429 -0
  9. package/bin/validate-agents-skills.js +251 -251
  10. package/bin/validate-agents.js +69 -69
  11. package/bin/validate-phase.js +263 -263
  12. package/bin/validate.js +369 -0
  13. package/content/.azure/README.md +293 -293
  14. package/content/.azure/docs/azure-devops-setup.md +454 -454
  15. package/content/.azure/docs/branch-strategy.md +398 -398
  16. package/content/.azure/docs/local-development.md +515 -515
  17. package/content/.azure/pipelines/pipeline-variables.yml +34 -34
  18. package/content/.azure/pipelines/prod-pipeline.yml +319 -319
  19. package/content/.azure/pipelines/staging-pipeline.yml +234 -234
  20. package/content/.azure/pipelines/templates/build-dotnet.yml +75 -75
  21. package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -94
  22. package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -120
  23. package/content/.azure/pipelines/templates/infra-deploy.yml +90 -90
  24. package/content/.claude/commands/morph-apply.md +221 -158
  25. package/content/.claude/commands/morph-archive.md +79 -79
  26. package/content/.claude/commands/morph-infra.md +209 -209
  27. package/content/.claude/commands/morph-preflight.md +227 -0
  28. package/content/.claude/commands/morph-proposal.md +122 -101
  29. package/content/.claude/commands/morph-status.md +86 -86
  30. package/content/.claude/commands/morph-troubleshoot.md +122 -0
  31. package/content/.claude/settings.local.json +15 -15
  32. package/content/.claude/skills/checklists/code-review.md +226 -0
  33. package/content/.claude/skills/checklists/morph-checklist.md +117 -0
  34. package/content/.claude/skills/checklists/simulation-checklist.md +77 -0
  35. package/content/.claude/skills/infra/bicep-architect.md +126 -419
  36. package/content/.claude/skills/infra/container-specialist.md +131 -437
  37. package/content/.claude/skills/infra/devops-engineer.md +119 -405
  38. package/content/.claude/skills/integrations/asaas-financial.md +130 -333
  39. package/content/.claude/skills/integrations/azure-identity.md +142 -309
  40. package/content/.claude/skills/integrations/clerk-auth.md +108 -290
  41. package/content/.claude/skills/integrations/resend-email.md +119 -0
  42. package/content/.claude/skills/specialists/ai-system-architect.md +192 -604
  43. package/content/.claude/skills/specialists/azure-architect.md +142 -142
  44. package/content/.claude/skills/specialists/code-analyzer.md +235 -0
  45. package/content/.claude/skills/specialists/dotnet-senior.md +287 -0
  46. package/content/.claude/skills/specialists/ef-modeler.md +113 -200
  47. package/content/.claude/skills/specialists/hangfire-orchestrator.md +126 -245
  48. package/content/.claude/skills/specialists/ms-agent-expert.md +109 -263
  49. package/content/.claude/skills/specialists/po-pm-advisor.md +197 -197
  50. package/content/.claude/skills/specialists/standards-architect.md +156 -78
  51. package/content/.claude/skills/specialists/testing-specialist.md +126 -0
  52. package/content/.claude/skills/specialists/ui-ux-designer.md +191 -1060
  53. package/content/.claude/skills/stacks/dotnet-blazor.md +210 -588
  54. package/content/.claude/skills/stacks/dotnet-nextjs.md +154 -402
  55. package/content/.claude/skills/workflows/morph-replicate.md +213 -0
  56. package/content/.claude/{commands/morph-clarify.md → skills/workflows/phase-clarify.md} +5 -58
  57. package/content/.claude/{commands/morph-design.md → skills/workflows/phase-design.md} +16 -86
  58. package/content/.claude/{commands/morph-setup.md → skills/workflows/phase-setup.md} +9 -17
  59. package/content/.claude/skills/workflows/phase-tasks.md +164 -0
  60. package/content/.claude/{commands/morph-uiux.md → skills/workflows/phase-uiux.md} +15 -88
  61. package/content/.morph/.morphversion +5 -5
  62. package/content/.morph/archive/.gitkeep +25 -25
  63. package/content/.morph/config/agents.json +378 -242
  64. package/content/.morph/config/config.template.json +89 -108
  65. package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -392
  66. package/content/.morph/docs/workflows/design-impl.md +37 -0
  67. package/content/.morph/docs/workflows/fast-track.md +29 -0
  68. package/content/.morph/docs/workflows/full-morph.md +76 -0
  69. package/content/.morph/docs/workflows/standard.md +44 -0
  70. package/content/.morph/docs/workflows/ui-refresh.md +39 -0
  71. package/content/.morph/examples/api-nextjs/README.md +241 -241
  72. package/content/.morph/examples/api-nextjs/contracts.ts +307 -307
  73. package/content/.morph/examples/api-nextjs/spec.md +399 -399
  74. package/content/.morph/examples/api-nextjs/tasks.md +168 -168
  75. package/content/.morph/examples/micro-saas/README.md +125 -125
  76. package/content/.morph/examples/micro-saas/contracts.cs +358 -358
  77. package/content/.morph/examples/micro-saas/decisions.md +246 -246
  78. package/content/.morph/examples/micro-saas/spec.md +236 -236
  79. package/content/.morph/examples/micro-saas/tasks.md +150 -150
  80. package/content/.morph/examples/multi-agent/README.md +309 -309
  81. package/content/.morph/examples/multi-agent/contracts.cs +433 -433
  82. package/content/.morph/examples/multi-agent/spec.md +479 -479
  83. package/content/.morph/examples/multi-agent/tasks.md +185 -185
  84. package/content/.morph/examples/scheduled-reports/decisions.md +158 -0
  85. package/content/.morph/examples/scheduled-reports/proposal.md +95 -0
  86. package/content/.morph/examples/scheduled-reports/spec.md +267 -0
  87. package/content/.morph/examples/state-v3.json +188 -0
  88. package/content/.morph/features/.gitkeep +25 -25
  89. package/content/.morph/hooks/README.md +190 -239
  90. package/content/.morph/hooks/pre-commit-agents.sh +24 -24
  91. package/content/.morph/hooks/pre-commit-all.sh +48 -48
  92. package/content/.morph/hooks/pre-commit-specs.sh +49 -49
  93. package/content/.morph/hooks/pre-commit-tests.sh +60 -60
  94. package/content/.morph/project.md +160 -160
  95. package/content/.morph/schemas/agent.schema.json +296 -296
  96. package/content/.morph/schemas/tasks.schema.json +220 -0
  97. package/content/.morph/specs/.gitkeep +20 -20
  98. package/content/.morph/standards/agent-framework-blazor-ui.md +359 -0
  99. package/content/.morph/standards/agent-framework-production.md +410 -0
  100. package/content/.morph/standards/agent-framework-setup.md +413 -453
  101. package/content/.morph/standards/agent-framework-workflows.md +349 -0
  102. package/content/.morph/standards/architecture.md +325 -325
  103. package/content/.morph/standards/azure.md +605 -379
  104. package/content/.morph/standards/coding.md +377 -377
  105. package/content/.morph/standards/dotnet10-migration.md +520 -494
  106. package/content/.morph/standards/fluent-ui-setup.md +590 -590
  107. package/content/.morph/standards/migration-guide.md +514 -514
  108. package/content/.morph/standards/passkeys-auth.md +423 -423
  109. package/content/.morph/standards/vector-search-rag.md +536 -536
  110. package/content/.morph/state.json +17 -17
  111. package/content/.morph/templates/FluentDesignTheme.cs +149 -149
  112. package/content/.morph/templates/MudTheme.cs +281 -281
  113. package/content/.morph/templates/agent.cs +163 -172
  114. package/content/.morph/templates/clarify-questions.md +159 -0
  115. package/content/.morph/templates/component.razor +239 -239
  116. package/content/.morph/templates/contracts/Commands.cs +74 -0
  117. package/content/.morph/templates/contracts/Entities.cs +25 -0
  118. package/content/.morph/templates/contracts/Queries.cs +74 -0
  119. package/content/.morph/templates/contracts/README.md +74 -0
  120. package/content/.morph/templates/contracts.cs +217 -217
  121. package/content/.morph/templates/decisions.md +123 -106
  122. package/content/.morph/templates/design-system.css +226 -226
  123. package/content/.morph/templates/infra/.dockerignore.example +89 -89
  124. package/content/.morph/templates/infra/Dockerfile.example +82 -82
  125. package/content/.morph/templates/infra/README.md +286 -286
  126. package/content/.morph/templates/infra/app-insights.bicep +63 -63
  127. package/content/.morph/templates/infra/app-service.bicep +164 -164
  128. package/content/.morph/templates/infra/container-app-env.bicep +49 -49
  129. package/content/.morph/templates/infra/container-app.bicep +156 -156
  130. package/content/.morph/templates/infra/deploy-checklist.md +426 -0
  131. package/content/.morph/templates/infra/deploy.ps1 +229 -229
  132. package/content/.morph/templates/infra/deploy.sh +208 -208
  133. package/content/.morph/templates/infra/key-vault.bicep +91 -91
  134. package/content/.morph/templates/infra/main.bicep +189 -189
  135. package/content/.morph/templates/infra/parameters.dev.json +29 -29
  136. package/content/.morph/templates/infra/parameters.prod.json +29 -29
  137. package/content/.morph/templates/infra/parameters.staging.json +29 -29
  138. package/content/.morph/templates/infra/sql-database.bicep +103 -103
  139. package/content/.morph/templates/infra/storage.bicep +106 -106
  140. package/content/.morph/templates/integrations/asaas-client.cs +387 -387
  141. package/content/.morph/templates/integrations/asaas-webhook.cs +351 -351
  142. package/content/.morph/templates/integrations/azure-identity-config.cs +288 -288
  143. package/content/.morph/templates/integrations/clerk-config.cs +258 -258
  144. package/content/.morph/templates/job.cs +171 -171
  145. package/content/.morph/templates/migration.cs +83 -83
  146. package/content/.morph/templates/proposal.md +141 -155
  147. package/content/.morph/templates/recap.md +94 -105
  148. package/content/.morph/templates/repository.cs +141 -141
  149. package/content/.morph/templates/saas/subscription.cs +347 -347
  150. package/content/.morph/templates/saas/tenant.cs +338 -338
  151. package/content/.morph/templates/service.cs +139 -139
  152. package/content/.morph/templates/simulation.md +353 -0
  153. package/content/.morph/templates/spec.md +149 -148
  154. package/content/.morph/templates/sprint-status.yaml +68 -68
  155. package/content/.morph/templates/state.template.json +222 -222
  156. package/content/.morph/templates/story.md +143 -143
  157. package/content/.morph/templates/tasks.md +257 -235
  158. package/content/.morph/templates/test.cs +239 -239
  159. package/content/.morph/templates/ui-components.md +362 -276
  160. package/content/.morph/templates/ui-design-system.md +286 -286
  161. package/content/.morph/templates/ui-flows.md +336 -336
  162. package/content/.morph/templates/ui-mockups.md +133 -133
  163. package/content/.morph/test-infra/example.bicep +59 -59
  164. package/content/CLAUDE.md +150 -442
  165. package/content/README.md +79 -79
  166. package/detectors/config-detector.js +223 -223
  167. package/detectors/conversation-analyzer.js +163 -163
  168. package/detectors/index.js +84 -84
  169. package/detectors/standards-generator.js +275 -275
  170. package/detectors/structure-detector.js +245 -250
  171. package/docs/README.md +144 -149
  172. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +977 -977
  173. package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1048 -1048
  174. package/docs/api/scripts/collapse.js +38 -38
  175. package/docs/api/scripts/commonNav.js +28 -28
  176. package/docs/api/scripts/linenumber.js +25 -25
  177. package/docs/api/scripts/nav.js +12 -12
  178. package/docs/api/scripts/polyfill.js +3 -3
  179. package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -202
  180. package/docs/api/scripts/prettify/lang-css.js +2 -2
  181. package/docs/api/scripts/prettify/prettify.js +28 -28
  182. package/docs/api/scripts/search.js +98 -98
  183. package/docs/api/styles/jsdoc.css +776 -776
  184. package/docs/api/styles/prettify.css +80 -80
  185. package/docs/examples.md +328 -328
  186. package/docs/getting-started.md +301 -302
  187. package/docs/installation.md +361 -361
  188. package/docs/templates.md +418 -418
  189. package/docs/validation-checklist.md +265 -266
  190. package/package.json +80 -80
  191. package/scripts/postinstall.js +132 -132
  192. package/src/commands/advance-phase.js +183 -0
  193. package/src/commands/analyze-blazor-concurrency.js +193 -0
  194. package/src/commands/create-story.js +351 -351
  195. package/src/commands/detect-agents.js +139 -0
  196. package/src/commands/detect.js +104 -104
  197. package/src/commands/doctor.js +356 -280
  198. package/src/commands/generate.js +149 -149
  199. package/src/commands/init.js +258 -245
  200. package/src/commands/lint-fluent.js +352 -0
  201. package/src/commands/rollback-phase.js +185 -0
  202. package/src/commands/session-summary.js +291 -0
  203. package/src/commands/shard-spec.js +224 -224
  204. package/src/commands/sprint-status.js +250 -250
  205. package/src/commands/state.js +333 -333
  206. package/src/commands/sync.js +167 -167
  207. package/src/commands/task.js +78 -0
  208. package/src/commands/troubleshoot.js +222 -0
  209. package/src/commands/update.js +192 -159
  210. package/src/commands/validate-blazor-state.js +210 -0
  211. package/src/commands/validate-blazor.js +156 -0
  212. package/src/commands/validate-css.js +84 -0
  213. package/src/commands/validate-phase.js +221 -0
  214. package/src/lib/blazor-concurrency-analyzer.js +288 -0
  215. package/src/lib/blazor-state-validator.js +291 -0
  216. package/src/lib/blazor-validator.js +374 -0
  217. package/src/lib/complexity-analyzer.js +441 -292
  218. package/src/lib/continuous-validator.js +421 -0
  219. package/src/lib/css-validator.js +352 -0
  220. package/src/lib/decision-constraint-loader.js +109 -0
  221. package/src/lib/design-system-generator.js +298 -298
  222. package/src/lib/learning-system.js +520 -0
  223. package/src/lib/mockup-generator.js +366 -0
  224. package/src/lib/recap-generator.js +205 -0
  225. package/src/lib/state-manager.js +397 -340
  226. package/src/lib/troubleshoot-grep.js +194 -0
  227. package/src/lib/troubleshoot-index.js +144 -0
  228. package/src/lib/ui-detector.js +350 -0
  229. package/src/lib/validation-runner.js +231 -0
  230. package/src/lib/validators/architecture-validator.js +387 -0
  231. package/src/lib/validators/contract-compliance-validator.js +273 -0
  232. package/src/lib/validators/package-validator.js +360 -0
  233. package/src/lib/validators/ui-contrast-validator.js +422 -0
  234. package/src/utils/file-copier.js +179 -139
  235. package/src/utils/logger.js +32 -32
  236. package/src/utils/version-checker.js +175 -175
  237. package/content/.claude/commands/morph-costs.md +0 -206
  238. package/content/.claude/commands/morph-tasks.md +0 -319
  239. package/content/.claude/skills/specialists/cost-guardian.md +0 -110
  240. package/content/.claude/skills/stacks/shopify.md +0 -445
  241. package/content/.morph/config/azure-pricing.json +0 -70
  242. package/content/.morph/config/azure-pricing.schema.json +0 -50
  243. package/content/.morph/hooks/pre-commit-costs.sh +0 -91
  244. package/docs/api/cost-calculator.js.html +0 -513
  245. package/docs/api/design-system-generator.js.html +0 -382
  246. package/docs/api/global.html +0 -5263
  247. package/docs/api/index.html +0 -96
  248. package/docs/api/state-manager.js.html +0 -423
  249. package/src/commands/cost.js +0 -181
  250. package/src/commands/update-pricing.js +0 -206
  251. package/src/lib/cost-calculator.js +0 -429
@@ -1,340 +1,397 @@
1
- /**
2
- * MORPH-SPEC State Manager Library
3
- *
4
- * Manages state.json for tracking features, progress, agents, and checkpoints.
5
- * Used both by CLI commands and internal automation.
6
- */
7
-
8
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
9
- import { join, dirname } from 'path';
10
-
11
- const STATE_FILE_NAME = '.morph/state.json';
12
-
13
- // ============================================================================
14
- // Core Functions
15
- // ============================================================================
16
-
17
- /**
18
- * Get the state file path (looks in current working directory)
19
- */
20
- export function getStatePath() {
21
- return join(process.cwd(), STATE_FILE_NAME);
22
- }
23
-
24
- /**
25
- * Check if state file exists
26
- */
27
- export function stateExists() {
28
- return existsSync(getStatePath());
29
- }
30
-
31
- /**
32
- * Load state from disk
33
- * @param {boolean} throwOnError - If false, returns null instead of throwing
34
- * @returns {Object|null} State object or null
35
- */
36
- export function loadState(throwOnError = true) {
37
- const statePath = getStatePath();
38
-
39
- if (!existsSync(statePath)) {
40
- if (throwOnError) {
41
- throw new Error(`State file not found: ${statePath}\nRun 'morph-spec state init' first.`);
42
- }
43
- return null;
44
- }
45
-
46
- try {
47
- const content = readFileSync(statePath, 'utf8');
48
- return JSON.parse(content);
49
- } catch (err) {
50
- if (throwOnError) {
51
- throw new Error(`Failed to parse state.json: ${err.message}`);
52
- }
53
- return null;
54
- }
55
- }
56
-
57
- /**
58
- * Save state to disk
59
- * @param {Object} state - State object to save
60
- */
61
- export function saveState(state) {
62
- state.metadata = state.metadata || {};
63
- state.metadata.lastUpdated = new Date().toISOString();
64
-
65
- const statePath = getStatePath();
66
- const stateDir = dirname(statePath);
67
-
68
- // Ensure .morph directory exists
69
- if (!existsSync(stateDir)) {
70
- mkdirSync(stateDir, { recursive: true });
71
- }
72
-
73
- writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
74
- }
75
-
76
- /**
77
- * Initialize new state file
78
- * @param {Object} options - Options
79
- * @param {boolean} options.force - Overwrite existing file
80
- * @param {string} options.projectName - Project name
81
- * @param {string} options.projectType - Project type (e.g., 'blazor-server')
82
- * @returns {Object} Initial state
83
- */
84
- export function initState(options = {}) {
85
- const { force = false, projectName = '{PROJECT_NAME}', projectType = 'blazor-server' } = options;
86
-
87
- if (stateExists() && !force) {
88
- throw new Error('State file already exists. Use force=true to overwrite.');
89
- }
90
-
91
- const initialState = {
92
- version: "2.1.1",
93
- project: {
94
- name: projectName,
95
- type: projectType,
96
- createdAt: new Date().toISOString(),
97
- updatedAt: new Date().toISOString()
98
- },
99
- features: {},
100
- metadata: {
101
- totalFeatures: 0,
102
- completedFeatures: 0,
103
- totalCostEstimated: 0,
104
- totalTimeSpent: 0,
105
- lastUpdated: new Date().toISOString()
106
- }
107
- };
108
-
109
- saveState(initialState);
110
- return initialState;
111
- }
112
-
113
- // ============================================================================
114
- // Feature Operations
115
- // ============================================================================
116
-
117
- /**
118
- * Get feature from state
119
- * @param {string} featureName - Feature name
120
- * @returns {Object|null} Feature object or null
121
- */
122
- export function getFeature(featureName) {
123
- const state = loadState();
124
- return state.features[featureName] || null;
125
- }
126
-
127
- /**
128
- * Create or get feature with default structure
129
- * @param {string} featureName - Feature name
130
- * @returns {Object} Feature object
131
- */
132
- function ensureFeature(featureName) {
133
- const state = loadState();
134
-
135
- if (!state.features[featureName]) {
136
- state.features[featureName] = {
137
- status: "draft",
138
- phase: "proposal",
139
- workflow: "auto", // auto | fast-track | standard | full-morph
140
- createdAt: new Date().toISOString(),
141
- updatedAt: new Date().toISOString(),
142
- activeAgents: [],
143
- outputs: {
144
- proposal: { created: false, path: `.morph/project/outputs/${featureName}/proposal.md` },
145
- spec: { created: false, path: `.morph/project/outputs/${featureName}/spec.md` },
146
- contracts: { created: false, path: `.morph/project/outputs/${featureName}/contracts.cs` },
147
- tasks: { created: false, path: `.morph/project/outputs/${featureName}/tasks.json` },
148
- uiDesignSystem: { created: false, path: `.morph/project/outputs/${featureName}/ui-design-system.md` },
149
- uiMockups: { created: false, path: `.morph/project/outputs/${featureName}/ui-mockups.md` },
150
- uiComponents: { created: false, path: `.morph/project/outputs/${featureName}/ui-components.md` },
151
- uiFlows: { created: false, path: `.morph/project/outputs/${featureName}/ui-flows.md` },
152
- decisions: { created: false, path: `.morph/project/outputs/${featureName}/decisions.md` },
153
- recap: { created: false, path: `.morph/project/outputs/${featureName}/recap.md` }
154
- },
155
- tasks: {
156
- total: 0,
157
- completed: 0,
158
- inProgress: 0,
159
- pending: 0
160
- },
161
- checkpoints: [],
162
- costs: {
163
- estimated: 0,
164
- approved: false,
165
- approvedBy: null,
166
- approvedAt: null
167
- }
168
- };
169
-
170
- state.metadata.totalFeatures++;
171
- saveState(state);
172
- }
173
-
174
- return state.features[featureName];
175
- }
176
-
177
- /**
178
- * Update feature property (supports nested keys like "tasks.completed")
179
- * @param {string} featureName - Feature name
180
- * @param {string} key - Property key (supports dot notation)
181
- * @param {any} value - Value to set
182
- */
183
- export function updateFeature(featureName, key, value) {
184
- ensureFeature(featureName);
185
- const state = loadState(); // Load AFTER ensuring feature exists
186
-
187
- const keys = key.split('.');
188
- let target = state.features[featureName];
189
-
190
- for (let i = 0; i < keys.length - 1; i++) {
191
- if (!target[keys[i]]) {
192
- target[keys[i]] = {};
193
- }
194
- target = target[keys[i]];
195
- }
196
-
197
- const finalKey = keys[keys.length - 1];
198
- target[finalKey] = value;
199
- state.features[featureName].updatedAt = new Date().toISOString();
200
-
201
- saveState(state);
202
- }
203
-
204
- /**
205
- * Update multiple feature properties at once
206
- * @param {string} featureName - Feature name
207
- * @param {Object} updates - Object with key-value pairs to update
208
- */
209
- export function updateFeatureMultiple(featureName, updates) {
210
- ensureFeature(featureName);
211
- const state = loadState(); // Load AFTER ensuring feature exists
212
-
213
- for (const [key, value] of Object.entries(updates)) {
214
- const keys = key.split('.');
215
- let target = state.features[featureName];
216
-
217
- for (let i = 0; i < keys.length - 1; i++) {
218
- if (!target[keys[i]]) {
219
- target[keys[i]] = {};
220
- }
221
- target = target[keys[i]];
222
- }
223
-
224
- const finalKey = keys[keys.length - 1];
225
- target[finalKey] = value;
226
- }
227
-
228
- state.features[featureName].updatedAt = new Date().toISOString();
229
- saveState(state);
230
- }
231
-
232
- /**
233
- * Add checkpoint to feature
234
- * @param {string} featureName - Feature name
235
- * @param {string} note - Checkpoint note
236
- * @returns {Object} Checkpoint object
237
- */
238
- export function addCheckpoint(featureName, note) {
239
- ensureFeature(featureName);
240
- const state = loadState();
241
- const feature = state.features[featureName];
242
-
243
- const checkpoint = {
244
- timestamp: new Date().toISOString(),
245
- phase: feature.phase,
246
- completedTasks: feature.tasks.completed,
247
- note: note
248
- };
249
-
250
- feature.checkpoints.push(checkpoint);
251
- feature.updatedAt = new Date().toISOString();
252
-
253
- saveState(state);
254
- return checkpoint;
255
- }
256
-
257
- /**
258
- * Add agent to feature
259
- * @param {string} featureName - Feature name
260
- * @param {string} agentId - Agent ID
261
- * @returns {boolean} True if added, false if already exists
262
- */
263
- export function addAgent(featureName, agentId) {
264
- ensureFeature(featureName);
265
- const state = loadState(); // Load AFTER ensuring feature exists
266
-
267
- if (!state.features[featureName].activeAgents.includes(agentId)) {
268
- state.features[featureName].activeAgents.push(agentId);
269
- state.features[featureName].updatedAt = new Date().toISOString();
270
- saveState(state);
271
- return true;
272
- }
273
-
274
- return false;
275
- }
276
-
277
- /**
278
- * Remove agent from feature
279
- * @param {string} featureName - Feature name
280
- * @param {string} agentId - Agent ID
281
- * @returns {boolean} True if removed, false if not found
282
- */
283
- export function removeAgent(featureName, agentId) {
284
- const state = loadState();
285
-
286
- if (!state.features[featureName]) {
287
- throw new Error(`Feature '${featureName}' not found.`);
288
- }
289
-
290
- const index = state.features[featureName].activeAgents.indexOf(agentId);
291
- if (index > -1) {
292
- state.features[featureName].activeAgents.splice(index, 1);
293
- state.features[featureName].updatedAt = new Date().toISOString();
294
- saveState(state);
295
- return true;
296
- }
297
-
298
- return false;
299
- }
300
-
301
- /**
302
- * Mark output as created
303
- * @param {string} featureName - Feature name
304
- * @param {string} outputType - Output type (proposal, spec, contracts, etc.)
305
- */
306
- export function markOutput(featureName, outputType) {
307
- ensureFeature(featureName);
308
- const state = loadState();
309
-
310
- if (!state.features[featureName].outputs[outputType]) {
311
- throw new Error(`Output type '${outputType}' not valid. Valid types: proposal, spec, contracts, tasks, uiDesignSystem, uiMockups, uiComponents, uiFlows, decisions, recap`);
312
- }
313
-
314
- state.features[featureName].outputs[outputType].created = true;
315
- state.features[featureName].updatedAt = new Date().toISOString();
316
-
317
- saveState(state);
318
- }
319
-
320
- /**
321
- * List all features
322
- * @returns {Array} Array of [featureName, featureObject] tuples
323
- */
324
- export function listFeatures() {
325
- const state = loadState();
326
- return Object.entries(state.features);
327
- }
328
-
329
- /**
330
- * Get project summary
331
- * @returns {Object} Summary with metadata
332
- */
333
- export function getSummary() {
334
- const state = loadState();
335
- return {
336
- project: state.project,
337
- metadata: state.metadata,
338
- featuresCount: Object.keys(state.features).length
339
- };
340
- }
1
+ /**
2
+ * MORPH-SPEC State Manager Library
3
+ *
4
+ * Manages state.json for tracking features, progress, agents, and checkpoints.
5
+ * Used both by CLI commands and internal automation.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
9
+ import { join, dirname } from 'path';
10
+
11
+ const STATE_FILE_NAME = '.morph/state.json';
12
+
13
+ // ============================================================================
14
+ // Core Functions
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Get the state file path (looks in current working directory)
19
+ */
20
+ export function getStatePath() {
21
+ return join(process.cwd(), STATE_FILE_NAME);
22
+ }
23
+
24
+ /**
25
+ * Check if state file exists
26
+ */
27
+ export function stateExists() {
28
+ return existsSync(getStatePath());
29
+ }
30
+
31
+ /**
32
+ * Load state from disk
33
+ * @param {boolean} throwOnError - If false, returns null instead of throwing
34
+ * @returns {Object|null} State object or null
35
+ */
36
+ export function loadState(throwOnError = true) {
37
+ const statePath = getStatePath();
38
+
39
+ if (!existsSync(statePath)) {
40
+ if (throwOnError) {
41
+ throw new Error(`State file not found: ${statePath}\nRun 'morph-spec state init' first.`);
42
+ }
43
+ return null;
44
+ }
45
+
46
+ try {
47
+ const content = readFileSync(statePath, 'utf8');
48
+ return JSON.parse(content);
49
+ } catch (err) {
50
+ if (throwOnError) {
51
+ throw new Error(`Failed to parse state.json: ${err.message}`);
52
+ }
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Save state to disk
59
+ * @param {Object} state - State object to save
60
+ */
61
+ export function saveState(state) {
62
+ state.metadata = state.metadata || {};
63
+ state.metadata.lastUpdated = new Date().toISOString();
64
+
65
+ const statePath = getStatePath();
66
+ const stateDir = dirname(statePath);
67
+
68
+ // Ensure .morph directory exists
69
+ if (!existsSync(stateDir)) {
70
+ mkdirSync(stateDir, { recursive: true });
71
+ }
72
+
73
+ writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
74
+ }
75
+
76
+ /**
77
+ * Initialize new state file
78
+ * @param {Object} options - Options
79
+ * @param {boolean} options.force - Overwrite existing file
80
+ * @param {string} options.projectName - Project name
81
+ * @param {string} options.projectType - Project type (e.g., 'blazor-server')
82
+ * @returns {Object} Initial state
83
+ */
84
+ export function initState(options = {}) {
85
+ const { force = false, projectName = '{PROJECT_NAME}', projectType = 'blazor-server' } = options;
86
+
87
+ if (stateExists() && !force) {
88
+ throw new Error('State file already exists. Use force=true to overwrite.');
89
+ }
90
+
91
+ const initialState = {
92
+ version: "2.1.1",
93
+ project: {
94
+ name: projectName,
95
+ type: projectType,
96
+ createdAt: new Date().toISOString(),
97
+ updatedAt: new Date().toISOString()
98
+ },
99
+ features: {},
100
+ metadata: {
101
+ totalFeatures: 0,
102
+ completedFeatures: 0,
103
+ totalTimeSpent: 0,
104
+ lastUpdated: new Date().toISOString()
105
+ }
106
+ };
107
+
108
+ saveState(initialState);
109
+ return initialState;
110
+ }
111
+
112
+ // ============================================================================
113
+ // Feature Operations
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Get feature from state
118
+ * @param {string} featureName - Feature name
119
+ * @returns {Object|null} Feature object or null
120
+ */
121
+ export function getFeature(featureName) {
122
+ const state = loadState();
123
+ return state.features[featureName] || null;
124
+ }
125
+
126
+ /**
127
+ * Create or get feature with default structure
128
+ * @param {string} featureName - Feature name
129
+ * @returns {Object} Feature object
130
+ */
131
+ function ensureFeature(featureName) {
132
+ const state = loadState();
133
+
134
+ if (!state.features[featureName]) {
135
+ state.features[featureName] = {
136
+ status: "draft",
137
+ phase: "proposal",
138
+ workflow: "auto", // auto | fast-track | standard | full-morph
139
+ createdAt: new Date().toISOString(),
140
+ updatedAt: new Date().toISOString(),
141
+ activeAgents: [],
142
+ outputs: {
143
+ proposal: { created: false, path: `.morph/project/outputs/${featureName}/proposal.md` },
144
+ spec: { created: false, path: `.morph/project/outputs/${featureName}/spec.md` },
145
+ contracts: { created: false, path: `.morph/project/outputs/${featureName}/contracts.cs` },
146
+ tasks: { created: false, path: `.morph/project/outputs/${featureName}/tasks.json` },
147
+ uiDesignSystem: { created: false, path: `.morph/project/outputs/${featureName}/ui-design-system.md` },
148
+ uiMockups: { created: false, path: `.morph/project/outputs/${featureName}/ui-mockups.md` },
149
+ uiComponents: { created: false, path: `.morph/project/outputs/${featureName}/ui-components.md` },
150
+ uiFlows: { created: false, path: `.morph/project/outputs/${featureName}/ui-flows.md` },
151
+ decisions: { created: false, path: `.morph/project/outputs/${featureName}/decisions.md` },
152
+ recap: { created: false, path: `.morph/project/outputs/${featureName}/recap.md` }
153
+ },
154
+ tasks: {
155
+ total: 0,
156
+ completed: 0,
157
+ inProgress: 0,
158
+ pending: 0
159
+ },
160
+ checkpoints: []
161
+ };
162
+
163
+ state.metadata.totalFeatures++;
164
+ saveState(state);
165
+ }
166
+
167
+ return state.features[featureName];
168
+ }
169
+
170
+ /**
171
+ * Update feature property (supports nested keys like "tasks.completed")
172
+ * @param {string} featureName - Feature name
173
+ * @param {string} key - Property key (supports dot notation)
174
+ * @param {any} value - Value to set
175
+ */
176
+ export function updateFeature(featureName, key, value) {
177
+ ensureFeature(featureName);
178
+ const state = loadState(); // Load AFTER ensuring feature exists
179
+
180
+ const keys = key.split('.');
181
+ let target = state.features[featureName];
182
+
183
+ for (let i = 0; i < keys.length - 1; i++) {
184
+ if (!target[keys[i]]) {
185
+ target[keys[i]] = {};
186
+ }
187
+ target = target[keys[i]];
188
+ }
189
+
190
+ const finalKey = keys[keys.length - 1];
191
+ target[finalKey] = value;
192
+ state.features[featureName].updatedAt = new Date().toISOString();
193
+
194
+ saveState(state);
195
+ }
196
+
197
+ /**
198
+ * Update multiple feature properties at once
199
+ * @param {string} featureName - Feature name
200
+ * @param {Object} updates - Object with key-value pairs to update
201
+ */
202
+ export function updateFeatureMultiple(featureName, updates) {
203
+ ensureFeature(featureName);
204
+ const state = loadState(); // Load AFTER ensuring feature exists
205
+
206
+ for (const [key, value] of Object.entries(updates)) {
207
+ const keys = key.split('.');
208
+ let target = state.features[featureName];
209
+
210
+ for (let i = 0; i < keys.length - 1; i++) {
211
+ if (!target[keys[i]]) {
212
+ target[keys[i]] = {};
213
+ }
214
+ target = target[keys[i]];
215
+ }
216
+
217
+ const finalKey = keys[keys.length - 1];
218
+ target[finalKey] = value;
219
+ }
220
+
221
+ state.features[featureName].updatedAt = new Date().toISOString();
222
+ saveState(state);
223
+ }
224
+
225
+ /**
226
+ * Add checkpoint to feature
227
+ * @param {string} featureName - Feature name
228
+ * @param {string} note - Checkpoint note
229
+ * @returns {Object} Checkpoint object
230
+ */
231
+ export function addCheckpoint(featureName, note) {
232
+ ensureFeature(featureName);
233
+ const state = loadState();
234
+ const feature = state.features[featureName];
235
+
236
+ const checkpoint = {
237
+ timestamp: new Date().toISOString(),
238
+ phase: feature.phase,
239
+ completedTasks: feature.tasks.completed,
240
+ note: note
241
+ };
242
+
243
+ feature.checkpoints.push(checkpoint);
244
+ feature.updatedAt = new Date().toISOString();
245
+
246
+ saveState(state);
247
+ return checkpoint;
248
+ }
249
+
250
+ /**
251
+ * Add agent to feature
252
+ * @param {string} featureName - Feature name
253
+ * @param {string} agentId - Agent ID
254
+ * @returns {boolean} True if added, false if already exists
255
+ */
256
+ export function addAgent(featureName, agentId) {
257
+ ensureFeature(featureName);
258
+ const state = loadState(); // Load AFTER ensuring feature exists
259
+
260
+ if (!state.features[featureName].activeAgents.includes(agentId)) {
261
+ state.features[featureName].activeAgents.push(agentId);
262
+ state.features[featureName].updatedAt = new Date().toISOString();
263
+ saveState(state);
264
+ return true;
265
+ }
266
+
267
+ return false;
268
+ }
269
+
270
+ /**
271
+ * Remove agent from feature
272
+ * @param {string} featureName - Feature name
273
+ * @param {string} agentId - Agent ID
274
+ * @returns {boolean} True if removed, false if not found
275
+ */
276
+ export function removeAgent(featureName, agentId) {
277
+ const state = loadState();
278
+
279
+ if (!state.features[featureName]) {
280
+ throw new Error(`Feature '${featureName}' not found.`);
281
+ }
282
+
283
+ const index = state.features[featureName].activeAgents.indexOf(agentId);
284
+ if (index > -1) {
285
+ state.features[featureName].activeAgents.splice(index, 1);
286
+ state.features[featureName].updatedAt = new Date().toISOString();
287
+ saveState(state);
288
+ return true;
289
+ }
290
+
291
+ return false;
292
+ }
293
+
294
+ /**
295
+ * Mark output as created
296
+ * @param {string} featureName - Feature name
297
+ * @param {string} outputType - Output type (proposal, spec, contracts, etc.)
298
+ */
299
+ export function markOutput(featureName, outputType) {
300
+ ensureFeature(featureName);
301
+ const state = loadState();
302
+
303
+ if (!state.features[featureName].outputs[outputType]) {
304
+ throw new Error(`Output type '${outputType}' not valid. Valid types: proposal, spec, contracts, tasks, uiDesignSystem, uiMockups, uiComponents, uiFlows, decisions, recap`);
305
+ }
306
+
307
+ state.features[featureName].outputs[outputType].created = true;
308
+ state.features[featureName].updatedAt = new Date().toISOString();
309
+
310
+ // If marking tasks output, try to sync task count from state tasks array
311
+ if (outputType === 'tasks') {
312
+ syncTasksCount(state.features[featureName]);
313
+ }
314
+
315
+ saveState(state);
316
+ }
317
+
318
+ /**
319
+ * Sync tasks count from tasks array (if exists)
320
+ * @param {Object} feature - Feature object
321
+ */
322
+ function syncTasksCount(feature) {
323
+ // If feature has tasks array (schema 3.0), count them
324
+ if (feature.tasks && Array.isArray(feature.tasks)) {
325
+ const tasks = feature.tasks;
326
+ feature.progress = {
327
+ total: tasks.length,
328
+ completed: tasks.filter(t => t.status === 'completed').length,
329
+ inProgress: tasks.filter(t => t.status === 'in_progress').length,
330
+ pending: tasks.filter(t => t.status === 'pending').length,
331
+ percentage: tasks.length > 0 ? Math.round((tasks.filter(t => t.status === 'completed').length / tasks.length) * 100) : 0
332
+ };
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Skip a phase and record it
338
+ * @param {string} featureName - Feature name
339
+ * @param {string} phase - Phase to skip
340
+ * @param {string} reason - Reason for skipping
341
+ */
342
+ export function skipPhase(featureName, phase, reason = '') {
343
+ ensureFeature(featureName);
344
+ const state = loadState();
345
+
346
+ if (!state.features[featureName].skippedPhases) {
347
+ state.features[featureName].skippedPhases = [];
348
+ }
349
+
350
+ // Don't add duplicates
351
+ const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
352
+ if (!existing) {
353
+ state.features[featureName].skippedPhases.push({
354
+ phase,
355
+ reason,
356
+ timestamp: new Date().toISOString()
357
+ });
358
+ }
359
+
360
+ state.features[featureName].updatedAt = new Date().toISOString();
361
+ saveState(state);
362
+ }
363
+
364
+ /**
365
+ * Get skipped phases for a feature
366
+ * @param {string} featureName - Feature name
367
+ * @returns {Array} Array of skipped phases
368
+ */
369
+ export function getSkippedPhases(featureName) {
370
+ const state = loadState();
371
+ if (!state.features[featureName]) {
372
+ return [];
373
+ }
374
+ return state.features[featureName].skippedPhases || [];
375
+ }
376
+
377
+ /**
378
+ * List all features
379
+ * @returns {Array} Array of [featureName, featureObject] tuples
380
+ */
381
+ export function listFeatures() {
382
+ const state = loadState();
383
+ return Object.entries(state.features);
384
+ }
385
+
386
+ /**
387
+ * Get project summary
388
+ * @returns {Object} Summary with metadata
389
+ */
390
+ export function getSummary() {
391
+ const state = loadState();
392
+ return {
393
+ project: state.project,
394
+ metadata: state.metadata,
395
+ featuresCount: Object.keys(state.features).length
396
+ };
397
+ }