@polymorphism-tech/morph-spec 9.0.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +11 -63
  4. package/bin/validate.js +1 -39
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -1
  9. package/framework/MORPH.md +3 -3
  10. package/framework/agents.json +19 -11
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +22 -31
  14. package/framework/evals/review-checklist.md +12 -2
  15. package/framework/hooks/README.md +8 -44
  16. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  17. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  18. package/framework/hooks/claude-code/core/chain.js +4 -65
  19. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  20. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  21. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  22. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  23. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  24. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  25. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  26. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  27. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  28. package/framework/hooks/claude-code/core/state-sync.js +12 -68
  29. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  30. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  31. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  32. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  33. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  34. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  35. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  36. package/framework/hooks/claude-code/statusline.sh +11 -11
  37. package/framework/hooks/shared/activity-logger.js +9 -89
  38. package/framework/hooks/shared/agent-reap.js +2 -17
  39. package/framework/hooks/shared/build-check.js +6 -75
  40. package/framework/hooks/shared/claim-registry.js +9 -119
  41. package/framework/hooks/shared/command-tokens.js +6 -51
  42. package/framework/hooks/shared/comment-rules.js +5 -20
  43. package/framework/hooks/shared/comment-scanner.js +6 -25
  44. package/framework/hooks/shared/comment-stripper.js +11 -85
  45. package/framework/hooks/shared/e2e-check.js +0 -0
  46. package/framework/hooks/shared/errata.js +2 -15
  47. package/framework/hooks/shared/feature-locator.js +11 -95
  48. package/framework/hooks/shared/frontend-locations.js +1 -3
  49. package/framework/hooks/shared/gate-question.js +1 -7
  50. package/framework/hooks/shared/git-exclude.js +3 -20
  51. package/framework/hooks/shared/git-paths.js +8 -104
  52. package/framework/hooks/shared/graph-freshness.js +22 -166
  53. package/framework/hooks/shared/graph-refresh.js +21 -149
  54. package/framework/hooks/shared/heavy-command.js +4 -12
  55. package/framework/hooks/shared/hook-response.js +8 -83
  56. package/framework/hooks/shared/morph-cli.js +8 -52
  57. package/framework/hooks/shared/payload-utils.js +10 -132
  58. package/framework/hooks/shared/phase-utils.js +1 -42
  59. package/framework/hooks/shared/redact-secrets.js +4 -15
  60. package/framework/hooks/shared/reparse-scan.js +6 -66
  61. package/framework/hooks/shared/slice-progress.js +4 -23
  62. package/framework/hooks/shared/source-files.js +4 -17
  63. package/framework/hooks/shared/state-reader.js +13 -183
  64. package/framework/hooks/shared/state-writer.js +8 -52
  65. package/framework/hooks/shared/stdin-reader.js +1 -11
  66. package/framework/hooks/shared/telemetry-logger.js +7 -67
  67. package/framework/hooks/shared/transcript-usage.js +7 -29
  68. package/framework/hooks/shared/worktree-root.js +7 -29
  69. package/framework/rules/csharp-standards.md +1 -1
  70. package/framework/rules/frontend-standards.md +1 -0
  71. package/framework/rules/hangfire-standards.md +1 -1
  72. package/framework/rules/neon-standards.md +1 -1
  73. package/framework/rules/nextjs-standards.md +1 -1
  74. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  75. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  76. package/framework/skills/core/morph-apply/SKILL.md +4 -4
  77. package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
  78. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  79. package/framework/skills/core/morph-media/SKILL.md +1 -1
  80. package/framework/skills/core/morph-plan/SKILL.md +9 -7
  81. package/framework/skills/core/morph-review/SKILL.md +9 -6
  82. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  83. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  84. package/framework/standards/STANDARDS.json +36 -14
  85. package/framework/standards/STANDARDS.meta.json +16 -2
  86. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  87. package/framework/standards/ai-agents/agent-session.md +1 -1
  88. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  89. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  90. package/framework/standards/ai-agents/guardrails.md +1 -1
  91. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  92. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  93. package/framework/standards/ai-agents/production.md +1 -1
  94. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  95. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  96. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  97. package/framework/standards/ai-agents/setup.md +2 -2
  98. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  99. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  100. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  101. package/framework/standards/ai-agents/workflows.md +1 -1
  102. package/framework/standards/architecture/code-comments.md +1 -1
  103. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  104. package/framework/standards/backend/api/validation.md +1 -1
  105. package/framework/standards/backend/dotnet/testing.md +10 -4
  106. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  107. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  108. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  109. package/framework/standards/data/nosql/cache/redis.md +1 -1
  110. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  111. package/framework/standards/frontend/admin-app/forms.md +1 -1
  112. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  113. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  114. package/framework/standards/frontend/nextjs/testing.md +10 -6
  115. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  116. package/framework/templates/.idea/morph-templates.xml +92 -0
  117. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  118. package/framework/templates/CLAUDE.md.core.template +3 -3
  119. package/framework/templates/IDE-SNIPPETS.md +262 -262
  120. package/framework/templates/REGISTRY.json +9 -9
  121. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  122. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  123. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  124. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  125. package/framework/templates/docs/plan.md +3 -1
  126. package/framework/templates/docs/ui-mockups.md +4 -0
  127. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  128. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  129. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  130. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  131. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  132. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  133. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  134. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  135. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  136. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  137. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  138. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  139. package/framework/templates/state.template.json +17 -17
  140. package/framework/templates/ui/design-system.css +226 -226
  141. package/package.json +1 -1
  142. package/src/commands/context/context-refresh.js +0 -16
  143. package/src/commands/dashboard/dashboard.js +2 -8
  144. package/src/commands/e2e/e2e.js +17 -104
  145. package/src/commands/graph/affected.js +0 -14
  146. package/src/commands/graph/build.js +11 -50
  147. package/src/commands/graph/explain.js +0 -16
  148. package/src/commands/graph/status.js +1 -12
  149. package/src/commands/mcp/mcp-setup.js +4 -62
  150. package/src/commands/observability/cost.js +5 -57
  151. package/src/commands/observability/telemetry.js +1 -14
  152. package/src/commands/project/branch.js +4 -45
  153. package/src/commands/project/claim.js +4 -48
  154. package/src/commands/project/doctor.js +48 -302
  155. package/src/commands/project/env.js +1 -27
  156. package/src/commands/project/fleet.js +3 -32
  157. package/src/commands/project/init.js +4 -68
  158. package/src/commands/project/install-plugin-cmd.js +0 -1
  159. package/src/commands/project/standards.js +1 -62
  160. package/src/commands/project/status.js +0 -53
  161. package/src/commands/project/update.js +31 -239
  162. package/src/commands/project/worktree.js +108 -882
  163. package/src/commands/scaffold/scaffold.js +0 -25
  164. package/src/commands/state/advance.js +14 -85
  165. package/src/commands/state/approve.js +15 -97
  166. package/src/commands/state/archive.js +12 -64
  167. package/src/commands/state/auto-approval.js +2 -5
  168. package/src/commands/state/create.js +22 -109
  169. package/src/commands/state/delete.js +11 -63
  170. package/src/commands/state/finish.js +98 -475
  171. package/src/commands/state/gate-check.js +22 -132
  172. package/src/commands/state/gate-decision.js +3 -39
  173. package/src/commands/state/handoff.js +3 -30
  174. package/src/commands/state/list.js +14 -84
  175. package/src/commands/state/report-check.js +8 -58
  176. package/src/commands/state/retype.js +4 -26
  177. package/src/commands/validation/index.js +0 -3
  178. package/src/commands/validation/mutate.js +11 -11
  179. package/src/commands/validation/plan-check.js +4 -2
  180. package/src/commands/validation/tests-audit.js +7 -0
  181. package/src/commands/validation/validate-feature.js +10 -97
  182. package/src/commands/validation/verify.js +30 -113
  183. package/src/core/paths/output-schema.js +2 -101
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +0 -27
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +22 -304
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +0 -1
  191. package/src/core/work-types.js +1 -36
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  194. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +1 -5
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/main.jsx +13 -13
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  211. package/src/dashboard/app/pages/Features.jsx +1 -8
  212. package/src/dashboard/app/pages/Guide.jsx +0 -1
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +10 -69
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +0 -6
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +10 -66
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +2 -6
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/dashboard/vite.config.js +24 -24
  233. package/src/lib/ai/ai-checks.js +7 -52
  234. package/src/lib/ai/ai-project-scan.js +14 -108
  235. package/src/lib/ai/code-audit.js +7 -63
  236. package/src/lib/ai/pin-drift.js +8 -51
  237. package/src/lib/ai/registry-audit.js +11 -90
  238. package/src/lib/ci/ci-verify.js +4 -19
  239. package/src/lib/ci/github-ci.js +9 -37
  240. package/src/lib/ci/workflow-scaffold.js +14 -10
  241. package/src/lib/detectors/claude-config-detector.js +13 -74
  242. package/src/lib/detectors/index.js +4 -50
  243. package/src/lib/e2e/applicability.js +37 -0
  244. package/src/lib/e2e/compose-env.js +5 -66
  245. package/src/lib/e2e/stack-teardown.js +6 -40
  246. package/src/lib/generators/architecture-scanner.js +18 -156
  247. package/src/lib/generators/handoff-generator.js +9 -53
  248. package/src/lib/generators/scaffold-generator.js +5 -101
  249. package/src/lib/generators/settings-generator.js +4 -37
  250. package/src/lib/git/branch-classifier.js +11 -84
  251. package/src/lib/git/default-branch.js +14 -169
  252. package/src/lib/git/evidence-staleness.js +1 -0
  253. package/src/lib/git/gh.js +2 -17
  254. package/src/lib/git/pending-prs.js +6 -37
  255. package/src/lib/git/pr-files.js +3 -28
  256. package/src/lib/git/pr-index.js +3 -34
  257. package/src/lib/git/tree-fingerprint.js +3 -15
  258. package/src/lib/git/tree-state.js +6 -33
  259. package/src/lib/graph/god-nodes.js +2 -22
  260. package/src/lib/graph/graph-health.js +3 -24
  261. package/src/lib/graph/graph-loader.js +5 -56
  262. package/src/lib/graph/graphify-runtime.js +7 -37
  263. package/src/lib/graph/graphifyignore.js +8 -74
  264. package/src/lib/graph/sanitize.js +16 -136
  265. package/src/lib/heavy/cleanup.js +1 -1
  266. package/src/lib/installers/effects.js +8 -93
  267. package/src/lib/installers/mcp-installer.js +9 -169
  268. package/src/lib/installers/memory-installer.js +4 -27
  269. package/src/lib/installers/templates-reconcile.js +5 -60
  270. package/src/lib/mutation/adapters.js +6 -17
  271. package/src/lib/mutation/dotnet-introspection.js +20 -106
  272. package/src/lib/mutation/runner.js +23 -75
  273. package/src/lib/mutation/subset.js +1 -4
  274. package/src/lib/mutation/trx.js +0 -1
  275. package/src/lib/plan/artifact-vocabulary.js +5 -25
  276. package/src/lib/plan/checks.js +34 -33
  277. package/src/lib/plan/plan-parser.js +7 -8
  278. package/src/lib/projects/registry.js +7 -44
  279. package/src/lib/review-reports.js +11 -26
  280. package/src/lib/risk-detector.js +41 -173
  281. package/src/lib/specs/adr-index.js +6 -34
  282. package/src/lib/specs/frontmatter.js +2 -15
  283. package/src/lib/specs/gfm-table.js +1 -7
  284. package/src/lib/specs/revocations.js +1 -14
  285. package/src/lib/specs/screen-checklist.js +47 -124
  286. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +1 -8
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +1 -7
  291. package/src/lib/standards/sections.js +2 -13
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +21 -169
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -240
  296. package/src/lib/tasks/test-runner.js +43 -341
  297. package/src/lib/telemetry/metrics.js +7 -36
  298. package/src/lib/telemetry/telemetry-reader.js +13 -125
  299. package/src/lib/telemetry/verify-event.js +2 -30
  300. package/src/lib/tests-audit/audit.js +96 -0
  301. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  302. package/src/lib/validators/code-budget/index.js +0 -7
  303. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  304. package/src/lib/validators/contracts/index.js +0 -6
  305. package/src/lib/validators/nextjs/index.js +0 -5
  306. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  307. package/src/lib/validators/packages/index.js +0 -6
  308. package/src/lib/validators/packages/package-validator.js +6 -56
  309. package/src/lib/validators/reuse/index.js +0 -8
  310. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  311. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  312. package/src/lib/validators/traceability-check.js +5 -63
  313. package/src/lib/validators/validation-runner.js +21 -158
  314. package/src/lib/validators/verify-runner.js +85 -376
  315. package/src/lib/work-type-detector.js +5 -67
  316. package/src/lib/worktree/docker-audit.js +17 -136
  317. package/src/lib/worktree/env-allocator.js +10 -98
  318. package/src/lib/worktree/feature-truth.js +12 -115
  319. package/src/lib/worktree/finish-backup.js +32 -242
  320. package/src/lib/worktree/fleet-scanner.js +13 -95
  321. package/src/lib/worktree/ignored-triage.js +15 -91
  322. package/src/lib/worktree/vscode-settings.js +2 -3
  323. package/src/lib/worktree/worktree-audit.js +21 -213
  324. package/src/lib/worktree/worktree-include.js +3 -5
  325. package/src/scripts/global-install.js +3 -9
  326. package/src/scripts/install-plugin.js +1 -13
  327. package/src/scripts/setup-infra.js +17 -147
  328. package/src/utils/agents-installer.js +29 -276
  329. package/src/utils/banner.js +4 -25
  330. package/src/utils/claude-md-injector.js +1 -37
  331. package/src/utils/claude-settings-manager.js +1 -52
  332. package/src/utils/file-copier.js +11 -95
  333. package/src/utils/hooks-installer.js +21 -298
  334. package/src/utils/skills-installer.js +4 -101
  335. package/src/utils/version-checker.js +4 -83
  336. package/framework/commands/morph-proposal.md +0 -9
  337. package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
  338. package/framework/hooks/git/pre-commit/agents.sh +0 -25
  339. package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
  340. package/framework/hooks/git/pre-push/run-tests.sh +0 -44
  341. package/src/commands/utils/index.js +0 -3
  342. package/src/lib/detectors/design-system-detector.js +0 -188
  343. package/src/lib/generators/design-system-generator.js +0 -508
  344. package/src/lib/specs/old-format.js +0 -68
  345. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,10 +1,3 @@
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
1
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, rmdirSync, statSync, readdirSync } from 'fs';
9
2
  import { join, dirname } from 'path';
10
3
  import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
@@ -13,45 +6,24 @@ import { derivePhaseFrom, diskArtifactProbe } from '../../../framework/hooks/sha
13
6
  const STATE_FILE_NAME = '.morph/state.json';
14
7
  const FEATURES_DIR_NAME = '.morph/features';
15
8
  const FEATURE_FILE_NAME = 'feature.json';
16
- /**
17
- * The one spelling of the committed project config's location, relative to a
18
- * project root. Exported because three modules used to write it out by hand
19
- * (`readProjectFromConfig` here, `readMorphConfig` and `morphConfigPath` in
20
- * src/lib/tasks/) — and `morphConfigPath`'s own JSDoc promised the reader that
21
- * the writer and the reader "can never disagree about which file is meant",
22
- * a guarantee three copies of a string cannot give. Now they can't.
23
- */
24
9
  export const CONFIG_FILE_NAME = '.morph/config/config.json';
25
10
 
26
- // The on-disk state version. NOTE: this tracks the *feature-object* schema, which
27
- // is unchanged by the per-feature storage split — features are now persisted to
28
- // `.morph/features/{name}/feature.json` (authoritative, committed) with the root
29
- // `.morph/state.json` acting as a thin, derivable, gitignored index. Migration to
30
- // the split layout is detected by `feature.json` presence, not by this number.
11
+ // Versions the feature-object schema, not the storage layout: the per-feature
12
+ // split is detected by the presence of `feature.json`.
31
13
  const STATE_VERSION = '5.0.0';
32
14
 
33
- // ============================================================================
34
- // Core Functions
35
- // ============================================================================
36
-
37
- /**
38
- * Get the state file path (looks in current working directory)
39
- */
40
15
  export function getStatePath() {
41
16
  return join(process.cwd(), STATE_FILE_NAME);
42
17
  }
43
18
 
44
- /** Absolute path to the per-feature folder root (.morph/features). */
45
19
  function getFeaturesDir() {
46
20
  return join(process.cwd(), FEATURES_DIR_NAME);
47
21
  }
48
22
 
49
- /** Absolute path to a feature's authoritative feature.json. */
50
23
  function getFeatureJsonPath(featureName) {
51
24
  return join(getFeaturesDir(), featureName, FEATURE_FILE_NAME);
52
25
  }
53
26
 
54
- /** List feature names that have a folder on disk under .morph/features. */
55
27
  function listFeatureNamesOnDisk() {
56
28
  const dir = getFeaturesDir();
57
29
  if (!existsSync(dir)) return [];
@@ -64,7 +36,6 @@ function listFeatureNamesOnDisk() {
64
36
  }
65
37
  }
66
38
 
67
- /** Read a feature's feature.json, or null if missing/corrupt. */
68
39
  function readFeatureJson(featureName) {
69
40
  const path = getFeatureJsonPath(featureName);
70
41
  if (!existsSync(path)) return null;
@@ -75,7 +46,6 @@ function readFeatureJson(featureName) {
75
46
  }
76
47
  }
77
48
 
78
- /** Read project identity from the committed .morph/config/config.json, if present. */
79
49
  function readProjectFromConfig() {
80
50
  const path = join(process.cwd(), CONFIG_FILE_NAME);
81
51
  if (!existsSync(path)) return null;
@@ -92,10 +62,7 @@ function readProjectFromConfig() {
92
62
  }
93
63
  }
94
64
 
95
- /**
96
- * Atomic JSON write: per-process temp file then rename, with an EPERM retry loop
97
- * for the brief Windows lock race. Creates the parent directory if needed.
98
- */
65
+ // Per-process temp file then rename; the EPERM retry covers the brief Windows lock race.
99
66
  export function atomicWriteJson(targetPath, data) {
100
67
  const dir = dirname(targetPath);
101
68
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -112,41 +79,25 @@ export function atomicWriteJson(targetPath, data) {
112
79
  if (err.code === 'EPERM' && attempt < MAX_RETRIES) {
113
80
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (attempt + 1));
114
81
  } else {
115
- try { unlinkSync(tmpPath); } catch { /* ignore cleanup errors */ }
82
+ try { unlinkSync(tmpPath); } catch { }
116
83
  throw err;
117
84
  }
118
85
  }
119
86
  }
120
87
  }
121
88
 
122
- /**
123
- * Write a feature's authoritative feature.json (atomic).
124
- *
125
- * Skips the write entirely when the on-disk content is already byte-identical to
126
- * what we'd write. `saveState` hydrates and re-persists EVERY feature, so a CLI
127
- * mutation on feature A would otherwise rewrite sibling B's file from A's loaded
128
- * copy. Skipping unchanged siblings means A never touches B's authoritative file
129
- * in the common case — shrinking the cross-feature clobber window to nothing
130
- * unless B genuinely changed under A.
131
- *
132
- * A formatting mismatch is NOT free, though: the guard compares bytes, so a
133
- * sibling committed with a trailing newline fails it and gets rewritten. The
134
- * content is equivalent, but git sees ` M` on a file this caller never meant to
135
- * touch — enough to fail a whole-tree clean check (see the finish teardown's clearRootWorktreeRegistration).
136
- * And the guard only skips IDENTICAL content: a concurrent instance's CHANGED
137
- * write to a sibling would still be clobbered by the sweep.
138
- */
89
+ // saveState re-persists every feature; skipping byte-identical content keeps a
90
+ // mutation on one feature from rewriting (and clobbering) its siblings' files.
139
91
  function writeFeatureJson(featureName, feature) {
140
92
  const path = getFeatureJsonPath(featureName);
141
93
  try {
142
94
  if (existsSync(path) && readFileSync(path, 'utf8') === JSON.stringify(feature, null, 2)) {
143
- return; // unchanged — don't rewrite (and don't clobber a concurrent sibling write)
95
+ return;
144
96
  }
145
- } catch { /* fall through to a normal write */ }
97
+ } catch { }
146
98
  atomicWriteJson(path, feature);
147
99
  }
148
100
 
149
- /** A fresh, empty state scaffold (used when no root index exists yet). */
150
101
  function scaffoldState() {
151
102
  return {
152
103
  version: STATE_VERSION,
@@ -156,13 +107,7 @@ function scaffoldState() {
156
107
  metadata: { totalFeatures: 0, completedFeatures: 0, totalTimeSpent: 0 },
157
108
  };
158
109
  }
159
- /**
160
- * Derive current phase from filesystem — delegates to the hook-side rule
161
- * (`derivePhaseFrom`) so the CLI and the hooks never disagree on a feature.
162
- *
163
- * @param {string} featurePath - Absolute path to .morph/features/{feature}/
164
- * @returns {string} Phase name: 'review' | 'implement' | 'uiux' | 'plan'
165
- */
110
+ // Delegates to the hook-side rule so the CLI and the hooks never disagree on a phase.
166
111
  export function derivePhase(featurePath) {
167
112
  let feature = null;
168
113
  try {
@@ -172,14 +117,6 @@ export function derivePhase(featurePath) {
172
117
  return derivePhaseFrom(feature, diskArtifactProbe(featurePath));
173
118
  }
174
119
 
175
- /**
176
- * Derive output existence from filesystem — checks if each output file exists at its expected path.
177
- * Returns an object matching the old outputs shape for backwards-compatible display.
178
- *
179
- * @param {string} featureName - Feature name
180
- * @param {string} [baseDir] - Project base dir (defaults to cwd)
181
- * @returns {Object} Map of outputType -> { created: boolean, path: string }
182
- */
183
120
  export function deriveOutputs(featureName, baseDir = process.cwd()) {
184
121
  const outputPaths = getAllOutputPaths(featureName);
185
122
  const result = {};
@@ -190,25 +127,16 @@ export function deriveOutputs(featureName, baseDir = process.cwd()) {
190
127
  return result;
191
128
  }
192
129
 
193
- /**
194
- * Check if state file exists
195
- */
196
130
  export function stateExists() {
197
131
  return existsSync(getStatePath());
198
132
  }
199
133
 
200
- /**
201
- * Load state from disk
202
- * @param {boolean} throwOnError - If false, returns null instead of throwing
203
- * @returns {Object|null} State object or null
204
- */
205
134
  export function loadState(throwOnError = true) {
206
135
  const statePath = getStatePath();
207
136
  const rootExists = existsSync(statePath);
208
137
  const featureNames = listFeatureNamesOnDisk();
209
138
  const featuresExist = featureNames.some((name) => existsSync(getFeatureJsonPath(name)));
210
139
 
211
- // "No state" only when neither the root index nor any per-feature file exists.
212
140
  if (!rootExists && !featuresExist) {
213
141
  if (throwOnError) {
214
142
  throw new Error(`State file not found: ${statePath}\nRun 'morph-spec init' to initialize MORPH in this project.`);
@@ -233,19 +161,12 @@ export function loadState(throwOnError = true) {
233
161
  if (fromConfig) state.project = { ...(state.project || {}), ...fromConfig };
234
162
  }
235
163
 
236
- // Snapshot the on-disk version BEFORE the migration blocks below bump it. The
237
- // reconciliation loop uses it to tell a legacy monolith (a pre-v5 layout that
238
- // predates the split — materialize) from a current-version index entry whose
239
- // folder is gone (a deleted feature — prune). A missing root index is treated
240
- // as "no migration" (null): the loop is empty in that case (features hydrate
241
- // from disk), so the value only matters when the root index exists.
164
+ // Snapshot before the migrations below bump it: the reconcile loop tells a
165
+ // legacy monolith (materialize) from a deleted feature (prune) by this value.
242
166
  const fileVersion = rootExists ? state.version : null;
243
167
 
244
- // A missing root index means we must (re)generate it from the per-feature
245
- // files we're about to hydrate — this is the self-heal path after a wipe.
246
168
  let mutated = !rootExists;
247
169
 
248
- // Migrate v3.0.0 → v4.0.0 (new directory layout) — operates on root features.
249
170
  if (state.version === '3.0.0') {
250
171
  state.version = '4.0.0';
251
172
  for (const [name, feature] of Object.entries(state.features)) {
@@ -258,8 +179,6 @@ export function loadState(throwOnError = true) {
258
179
  mutated = true;
259
180
  }
260
181
 
261
- // Migrate v4.x → v5.0.0: drop derived fields (now filesystem-derived) and
262
- // workflow selection (v5 has fixed phases).
263
182
  if (state.version && state.version.startsWith('4.')) {
264
183
  state.version = '5.0.0';
265
184
  for (const feature of Object.values(state.features)) {
@@ -271,57 +190,35 @@ export function loadState(throwOnError = true) {
271
190
  mutated = true;
272
191
  }
273
192
 
274
- // Reconcile index entries whose feature.json is missing. The discriminator is
275
- // the ON-DISK VERSION (`fileVersion`, snapshotted before the migrations bumped
276
- // it) — NOT whether any sibling folder survived. Sibling-existence is the wrong
277
- // signal: deleting EVERY folder at once (the reported incident) would leave no
278
- // survivor and resurrect them all. Version is unconditional:
279
- //
280
- // • A pre-v5 root (v3/v4) or an entry rich beyond the thin {status,updatedAt}
281
- // shape = a legacy monolithic state.json that predates the split. It is the
282
- // sole copy of its feature — materialize each to its authoritative
283
- // feature.json (split-on-load migration; idempotent thereafter).
284
- //
285
- // • A current-version (v5) thin entry with NO folder = the feature was
286
- // DELETED and this gitignored index entry is stale. Prune it; NEVER recreate
287
- // the folder — that turned an intentional delete into a zombie. This is safe
288
- // and exhaustive because saveState ALWAYS co-writes feature.json + a thin
289
- // index entry: a v5 thin entry with no feature.json can ONLY mean the folder
290
- // was removed. The prune sticks — saveState below derives the index from this
291
- // same in-memory map, and hydration only re-adds features whose folder exists.
193
+ // Index entry without feature.json: a pre-v5 or rich entry is the sole copy of a
194
+ // legacy monolith (materialize it); a v5 thin entry means the folder was deleted
195
+ // (prune it, never resurrect). Safe because saveState always co-writes both.
292
196
  const isRichEntry = (f) =>
293
197
  f && typeof f === 'object' && Object.keys(f).some((k) => k !== 'status' && k !== 'updatedAt');
294
198
  const wasVersionMigration = fileVersion !== null && fileVersion !== STATE_VERSION;
295
199
  for (const [name, feature] of Object.entries(state.features)) {
296
200
  if (existsSync(getFeatureJsonPath(name))) continue;
297
201
  if (wasVersionMigration || isRichEntry(feature)) {
298
- writeFeatureJson(name, feature); // legacy monolith — materialize, don't lose it
202
+ writeFeatureJson(name, feature);
299
203
  } else {
300
- delete state.features[name]; // v5 thin entry, no folder = deleted — never resurrect
204
+ delete state.features[name];
301
205
  console.error(`ℹ morph-spec: dropped stale index entry "${name}" (feature folder was deleted).`);
302
206
  }
303
207
  mutated = true;
304
208
  }
305
209
 
306
- // Hydrate from per-feature files (authoritative). Also picks up features that
307
- // exist on disk but are absent from a stale/deleted root index — so a wiped
308
- // state.json self-heals from the committed feature.json files.
309
210
  for (const name of featureNames) {
310
211
  const fj = readFeatureJson(name);
311
212
  if (fj) state.features[name] = fj;
312
213
  }
313
214
 
314
215
  if (mutated) {
315
- saveState(state); // re-persists feature.json files + a freshly thinned root index
216
+ saveState(state);
316
217
  }
317
218
 
318
219
  return state;
319
220
  }
320
221
 
321
- /**
322
- * Save state to disk
323
- * @param {Object} state - State object to save
324
- */
325
222
  export function saveState(state) {
326
223
  state.metadata = state.metadata || {};
327
224
  state.metadata.lastUpdated = new Date().toISOString();
@@ -329,32 +226,18 @@ export function saveState(state) {
329
226
 
330
227
  const features = state.features || {};
331
228
 
332
- // Aggregate metadata is derived from the authoritative feature set so the
333
- // thin root index never drifts from the per-feature files.
334
229
  state.metadata.totalFeatures = Object.keys(features).length;
335
230
  state.metadata.completedFeatures = Object.values(features)
336
231
  .filter((f) => f && f.status === 'completed').length;
337
232
 
338
- // 1) Persist each feature to its authoritative, committed feature.json — what
339
- // travels with the feature's own commit.
340
- //
341
- // NOTE: this IS a cross-feature sweep. Mutating one feature re-persists
342
- // every sibling; writeFeatureJson's byte-identical guard makes that a no-op
343
- // only while a sibling's committed file matches our formatting exactly. A
344
- // sibling saved with a trailing newline (any editor, prettier) is rewritten
345
- // and shows up as ` M` — dirtying the tree on behalf of a feature the
346
- // caller never touched. Callers that need to change ONE feature's field
347
- // should write that feature.json directly instead of round-tripping state
348
- // through here (see the finish teardown's clearRootWorktreeRegistration).
233
+ // A cross-feature sweep: a sibling saved in another format (trailing newline) is
234
+ // rewritten and dirties the tree. To change one feature, write its feature.json directly.
349
235
  const thinFeatures = {};
350
236
  for (const [name, feature] of Object.entries(features)) {
351
237
  writeFeatureJson(name, feature);
352
238
  thinFeatures[name] = { status: feature.status, updatedAt: feature.updatedAt };
353
239
  }
354
240
 
355
- // 2) Persist the thin, derivable, gitignored root index (status + updatedAt
356
- // per feature is all that callers like getActiveFeature need from the root;
357
- // full feature data is hydrated from feature.json on load).
358
241
  atomicWriteJson(getStatePath(), {
359
242
  version: state.version,
360
243
  project: state.project,
@@ -364,20 +247,11 @@ export function saveState(state) {
364
247
  });
365
248
  }
366
249
 
367
- // ============================================================================
368
- // File Locking (BUG-002)
369
- // ============================================================================
370
-
371
250
  const LOCK_DIR_NAME = '.morph/state.json.lock';
372
251
  const LOCK_MAX_RETRIES = 5;
373
252
  const LOCK_BASE_MS = 100;
374
253
  const LOCK_STALE_MS = 30_000;
375
254
 
376
- /**
377
- * Wrap a read-modify-write cycle on state.json with a mkdir-based mutex.
378
- * @param {Function} fn - Async function to execute while holding the lock
379
- * @returns {Promise<*>} Return value of fn
380
- */
381
255
  export async function withStateLock(fn) {
382
256
  const lockPath = join(process.cwd(), LOCK_DIR_NAME);
383
257
 
@@ -408,14 +282,6 @@ export async function withStateLock(fn) {
408
282
  }
409
283
  }
410
284
 
411
- /**
412
- * Initialize new state file
413
- * @param {Object} options - Options
414
- * @param {boolean} options.force - Overwrite existing file
415
- * @param {string} options.projectName - Project name
416
- * @param {string} options.projectType - Project type (e.g., 'dotnet-nextjs')
417
- * @returns {Object} Initial state
418
- */
419
285
  export function initState(options = {}) {
420
286
  const { force = false, projectName = '{PROJECT_NAME}', projectType = 'dotnet-nextjs' } = options;
421
287
 
@@ -445,27 +311,11 @@ export function initState(options = {}) {
445
311
  return initialState;
446
312
  }
447
313
 
448
- // ============================================================================
449
- // Feature Operations
450
- // ============================================================================
451
-
452
- /**
453
- * Get feature from state
454
- * @param {string} featureName - Feature name
455
- * @returns {Object|null} Feature object or null
456
- */
457
314
  export function getFeature(featureName) {
458
315
  const state = loadState();
459
316
  return state.features[featureName] || null;
460
317
  }
461
318
 
462
- /**
463
- * Create or get feature with default structure.
464
- * v5: every feature follows the fixed phase sequence
465
- * (plan → [uiux] → implement → review). No workflow selection.
466
- * @param {string} featureName - Feature name
467
- * @returns {Promise<Object>} Feature object
468
- */
469
319
  async function ensureFeature(featureName) {
470
320
  const state = loadState();
471
321
 
@@ -492,28 +342,6 @@ async function ensureFeature(featureName) {
492
342
  return state.features[featureName];
493
343
  }
494
344
 
495
- /**
496
- * Explicitly create a feature with the canonical shape (documented in
497
- * framework/schemas/feature.schema.json). Idempotent: an existing feature is
498
- * returned untouched (including its `workType` — a later `--type` on a
499
- * repeat call never overwrites it; see `morph-spec retype` for that). This
500
- * is the server-side birth path behind `morph-spec create` — replacing the
501
- * implicit registration on the first Write into .morph/features/ (the
502
- * state-sync hook remains as fallback).
503
- *
504
- * Works even before any state exists (scaffolds the root index), so a
505
- * feature can be born as the very first morph action in a project.
506
- *
507
- * @param {string} featureName - Feature name (kebab-case), also the folder id
508
- * @param {{ status?: string, workType?: string, workTypeDetection?: object, name?: string, description?: string }} [options]
509
- * `name` defaults to `featureName` (the slug) — always stamped so a feature
510
- * is never nameless. `description` is written only when provided (the CLI
511
- * `create` enforces it; internal callers may omit it). `workType` defaults to
512
- * 'feature' (v8 routing — see src/core/work-types.js). `workTypeDetection` is
513
- * only written when provided (source/confidence/matched audit trail from the
514
- * `--type`/`--request` resolution in `create.js`).
515
- * @returns {Promise<{ feature: Object, created: boolean }>}
516
- */
517
345
  export async function createFeature(featureName, options = {}) {
518
346
  const { status = 'in_progress', workType, workTypeDetection, name, description } = options;
519
347
  const state = loadState(false) || scaffoldState();
@@ -552,12 +380,6 @@ export async function createFeature(featureName, options = {}) {
552
380
  return { feature: state.features[featureName], created: true };
553
381
  }
554
382
 
555
- /**
556
- * Update feature property (supports nested keys like "approvalGates.plan.approved")
557
- * @param {string} featureName - Feature name
558
- * @param {string} key - Property key (supports dot notation)
559
- * @param {any} value - Value to set
560
- */
561
383
  export async function updateFeature(featureName, key, value) {
562
384
  await ensureFeature(featureName);
563
385
  const state = loadState(); // Load AFTER ensuring feature exists
@@ -579,11 +401,6 @@ export async function updateFeature(featureName, key, value) {
579
401
  saveState(state);
580
402
  }
581
403
 
582
- /**
583
- * Update multiple feature properties at once
584
- * @param {string} featureName - Feature name
585
- * @param {Object} updates - Object with key-value pairs to update
586
- */
587
404
  export async function updateFeatureMultiple(featureName, updates) {
588
405
  await ensureFeature(featureName);
589
406
  const state = loadState(); // Load AFTER ensuring feature exists
@@ -607,33 +424,16 @@ export async function updateFeatureMultiple(featureName, updates) {
607
424
  saveState(state);
608
425
  }
609
426
 
610
- /**
611
- * Normalize output type from kebab-case to camelCase (BUG-002 fix - Enhanced)
612
- * Supports both kebab-case and camelCase input formats for all output types
613
- * @param {string} type - Output type (e.g., 'ui-design-system', 'stack-scan', or 'uiDesignSystem')
614
- * @returns {string} Normalized type in camelCase
615
- */
616
427
  function normalizeOutputType(type) {
617
- // If string doesn't contain hyphens, assume it's already camelCase
618
428
  if (!type.includes('-')) {
619
429
  return type;
620
430
  }
621
431
 
622
- // Convert kebab-case to camelCase automatically
623
- // Example: 'ui-design-system' → 'uiDesignSystem'
624
- // Example: 'stack-scan' → 'stackScan'
625
432
  const camelCase = type.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
626
433
 
627
434
  return camelCase;
628
435
  }
629
436
 
630
- /**
631
- * Calculate Levenshtein distance between two strings
632
- * Used for fuzzy matching and typo detection
633
- * @param {string} a - First string
634
- * @param {string} b - Second string
635
- * @returns {number} Edit distance
636
- */
637
437
  function levenshteinDistance(a, b) {
638
438
  const matrix = [];
639
439
 
@@ -651,9 +451,9 @@ function levenshteinDistance(a, b) {
651
451
  matrix[i][j] = matrix[i - 1][j - 1];
652
452
  } else {
653
453
  matrix[i][j] = Math.min(
654
- matrix[i - 1][j - 1] + 1, // substitution
655
- matrix[i][j - 1] + 1, // insertion
656
- matrix[i - 1][j] + 1 // deletion
454
+ matrix[i - 1][j - 1] + 1,
455
+ matrix[i][j - 1] + 1,
456
+ matrix[i - 1][j] + 1
657
457
  );
658
458
  }
659
459
  }
@@ -662,12 +462,6 @@ function levenshteinDistance(a, b) {
662
462
  return matrix[b.length][a.length];
663
463
  }
664
464
 
665
- /**
666
- * Find closest matching output type for error suggestions
667
- * @param {string} inputType - The invalid input type
668
- * @param {Array<string>} validTypes - List of valid output types
669
- * @returns {string|null} Closest match or null
670
- */
671
465
  function findClosestMatch(inputType, validTypes) {
672
466
  let closestMatch = null;
673
467
  let minDistance = Infinity;
@@ -677,14 +471,11 @@ function findClosestMatch(inputType, validTypes) {
677
471
  for (const validType of validTypes) {
678
472
  const distance = levenshteinDistance(normalizedInput, validType.toLowerCase());
679
473
 
680
- // Consider it a match if distance is less than 3 (likely typo)
681
- // or if one string contains the other (partial match)
682
474
  if (distance < minDistance && distance <= 3) {
683
475
  minDistance = distance;
684
476
  closestMatch = validType;
685
477
  }
686
478
 
687
- // Also check for partial matches
688
479
  if (validType.toLowerCase().includes(normalizedInput) ||
689
480
  normalizedInput.includes(validType.toLowerCase())) {
690
481
  if (distance < minDistance) {
@@ -694,28 +485,20 @@ function findClosestMatch(inputType, validTypes) {
694
485
  }
695
486
  }
696
487
 
697
- // Only suggest if similarity is reasonable
698
488
  return minDistance <= 3 ? closestMatch : null;
699
489
  }
700
490
 
701
- /**
702
- * Mark output as created
703
- * @param {string} featureName - Feature name
704
- * @param {string} outputType - Output type (plan, checks, verifierReport, etc.)
705
- */
706
491
  export async function markOutput(featureName, outputType) {
707
492
  await ensureFeature(featureName);
708
493
  const state = loadState();
709
494
 
710
495
  const normalized = normalizeOutputType(outputType);
711
496
 
712
- // Initialize outputs on demand (not pre-stored)
713
497
  if (!state.features[featureName].outputs) {
714
498
  state.features[featureName].outputs = getAllOutputPaths(featureName);
715
499
  }
716
500
 
717
501
  if (!state.features[featureName].outputs[normalized]) {
718
- // Try to find closest match for better error message
719
502
  const validCamelCaseTypes = Object.keys(state.features[featureName].outputs);
720
503
  const suggestion = findClosestMatch(outputType, validCamelCaseTypes);
721
504
 
@@ -723,7 +506,6 @@ export async function markOutput(featureName, outputType) {
723
506
 
724
507
  if (suggestion) {
725
508
  errorMsg += `\n\nDid you mean '${suggestion}'?`;
726
- // If the suggestion has a kebab-case alternative, show it too
727
509
  const kebabAlternative = {
728
510
  'uiDesignSystem': 'ui-design-system',
729
511
  'uiMockups': 'ui-mockups',
@@ -745,7 +527,6 @@ export async function markOutput(featureName, outputType) {
745
527
  state.features[featureName].outputs[normalized].created = true;
746
528
  state.features[featureName].updatedAt = new Date().toISOString();
747
529
 
748
- // Track the file change automatically
749
530
  const outputPath = state.features[featureName].outputs[normalized].path;
750
531
  if (outputPath) {
751
532
  if (!Array.isArray(state.features[featureName].fileChanges)) {
@@ -762,13 +543,6 @@ export async function markOutput(featureName, outputType) {
762
543
  saveState(state);
763
544
  }
764
545
 
765
- /**
766
- * Track a file change for a feature
767
- * @param {string} featureName - Feature name
768
- * @param {string} filePath - Path of the changed file
769
- * @param {string} action - 'created' | 'modified' | 'deleted'
770
- * @param {string} phase - Phase during which the change occurred
771
- */
772
546
  export async function trackFileChange(featureName, filePath, action, phase) {
773
547
  await ensureFeature(featureName);
774
548
  const state = loadState();
@@ -788,13 +562,6 @@ export async function trackFileChange(featureName, filePath, action, phase) {
788
562
  saveState(state);
789
563
  }
790
564
 
791
- /**
792
- * Get file changes for a feature, optionally grouped by phase
793
- * @param {string} featureName - Feature name
794
- * @param {Object} options - Options
795
- * @param {boolean} options.groupByPhase - Group changes by phase
796
- * @returns {Array|Object} File changes
797
- */
798
565
  export function getFileChanges(featureName, options = {}) {
799
566
  const state = loadState();
800
567
  const feature = state.features[featureName];
@@ -818,12 +585,6 @@ export function getFileChanges(featureName, options = {}) {
818
585
  return changes;
819
586
  }
820
587
 
821
- /**
822
- * Skip a phase and record it
823
- * @param {string} featureName - Feature name
824
- * @param {string} phase - Phase to skip
825
- * @param {string} reason - Reason for skipping
826
- */
827
588
  export async function skipPhase(featureName, phase, reason = '') {
828
589
  await ensureFeature(featureName);
829
590
  const state = loadState();
@@ -832,7 +593,6 @@ export async function skipPhase(featureName, phase, reason = '') {
832
593
  state.features[featureName].skippedPhases = [];
833
594
  }
834
595
 
835
- // Don't add duplicates
836
596
  const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
837
597
  if (!existing) {
838
598
  state.features[featureName].skippedPhases.push({
@@ -846,11 +606,6 @@ export async function skipPhase(featureName, phase, reason = '') {
846
606
  saveState(state);
847
607
  }
848
608
 
849
- /**
850
- * Get skipped phases for a feature
851
- * @param {string} featureName - Feature name
852
- * @returns {Array} Array of skipped phases
853
- */
854
609
  export function getSkippedPhases(featureName) {
855
610
  const state = loadState();
856
611
  if (!state.features[featureName]) {
@@ -859,19 +614,11 @@ export function getSkippedPhases(featureName) {
859
614
  return state.features[featureName].skippedPhases || [];
860
615
  }
861
616
 
862
- /**
863
- * List all features
864
- * @returns {Array} Array of [featureName, featureObject] tuples
865
- */
866
617
  export function listFeatures() {
867
618
  const state = loadState();
868
619
  return Object.entries(state.features);
869
620
  }
870
621
 
871
- /**
872
- * Get project summary
873
- * @returns {Object} Summary with metadata
874
- */
875
622
  export function getSummary() {
876
623
  const state = loadState();
877
624
  return {
@@ -881,17 +628,6 @@ export function getSummary() {
881
628
  };
882
629
  }
883
630
 
884
- // ============================================================================
885
- // Approval Gates Operations
886
- // ============================================================================
887
-
888
- /**
889
- * Set approval gate status
890
- * @param {string} featureName - Feature name
891
- * @param {string} gate - Gate name (plan, uiux, review)
892
- * @param {boolean} approved - Approval status
893
- * @param {Object} metadata - Additional metadata (approvedBy, reason, etc.)
894
- */
895
631
  export async function setApprovalGate(featureName, gate, approved, metadata = {}) {
896
632
  const state = loadState();
897
633
  const feature = await ensureFeature(featureName);
@@ -915,12 +651,6 @@ export async function setApprovalGate(featureName, gate, approved, metadata = {}
915
651
  saveState(state);
916
652
  }
917
653
 
918
- /**
919
- * Get approval gate status
920
- * @param {string} featureName - Feature name
921
- * @param {string} gate - Gate name
922
- * @returns {Object|null} Gate object or null
923
- */
924
654
  export function getApprovalGate(featureName, gate) {
925
655
  const state = loadState();
926
656
  const feature = state.features[featureName];
@@ -932,11 +662,6 @@ export function getApprovalGate(featureName, gate) {
932
662
  return feature.approvalGates[gate] || null;
933
663
  }
934
664
 
935
- /**
936
- * Check if feature is pending approval
937
- * @param {string} featureName - Feature name
938
- * @returns {boolean} True if any gate is pending approval
939
- */
940
665
  export function isPendingApproval(featureName) {
941
666
  const state = loadState();
942
667
  const feature = state.features[featureName];
@@ -947,7 +672,6 @@ export function isPendingApproval(featureName) {
947
672
 
948
673
  const currentPhase = feature.phase || 'plan';
949
674
 
950
- // Check if current phase has an approval gate
951
675
  const phaseGateMap = {
952
676
  'plan': 'plan',
953
677
  'review': 'review',
@@ -963,11 +687,6 @@ export function isPendingApproval(featureName) {
963
687
  return gate && !gate.approved;
964
688
  }
965
689
 
966
- /**
967
- * Get approval history for a feature
968
- * @param {string} featureName - Feature name
969
- * @returns {Array} Array of approval events
970
- */
971
690
  export function getApprovalHistory(featureName) {
972
691
  const state = loadState();
973
692
  const feature = state.features[featureName];
@@ -990,6 +709,5 @@ export function getApprovalHistory(featureName) {
990
709
  }
991
710
  });
992
711
 
993
- // Sort by timestamp
994
712
  return history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
995
713
  }