savepoint 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/.prettierignore +4 -0
  3. package/.savepoint/Design.md +196 -0
  4. package/.savepoint/PRD.md +58 -0
  5. package/.savepoint/audit/E01-go-setup/proposals.md +166 -0
  6. package/.savepoint/audit/E01-go-setup/snapshot.md +71 -0
  7. package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +66 -0
  8. package/.savepoint/audit/E01-scaffolding/proposals/Design.md +210 -0
  9. package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +117 -0
  10. package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +101 -0
  11. package/.savepoint/audit/E01-scaffolding/snapshot.md +54 -0
  12. package/.savepoint/audit/E02-data-model/snapshot.md +128 -0
  13. package/.savepoint/audit/E02-data-readers/proposals.md +123 -0
  14. package/.savepoint/audit/E02-data-readers/snapshot.md +54 -0
  15. package/.savepoint/audit/E03-board-tui-core/proposals.md +146 -0
  16. package/.savepoint/audit/E03-board-tui-core/snapshot.md +57 -0
  17. package/.savepoint/audit/E03-cli-foundation/snapshot.md +106 -0
  18. package/.savepoint/audit/E04-board-components/proposals.md +118 -0
  19. package/.savepoint/audit/E04-board-components/snapshot.md +77 -0
  20. package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +115 -0
  21. package/.savepoint/audit/E05-init-command/snapshot.md +125 -0
  22. package/.savepoint/audit/E05-phase-transitions/proposals.md +83 -0
  23. package/.savepoint/audit/E05-phase-transitions/snapshot.md +36 -0
  24. package/.savepoint/audit/E06-tui-board/snapshot.md +64 -0
  25. package/.savepoint/audit/E07-audit-pipeline/snapshot.md +165 -0
  26. package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +65 -0
  27. package/.savepoint/config.yml +27 -0
  28. package/.savepoint/releases/v1/PRD.md +66 -0
  29. package/.savepoint/releases/v1/epics/E01-go-setup/Design.md +39 -0
  30. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +42 -0
  31. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T002-entrypoint.md +23 -0
  32. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T003-directory-structure.md +24 -0
  33. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T004-makefile.md +23 -0
  34. package/.savepoint/releases/v1/epics/E02-data-readers/Design.md +61 -0
  35. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T001-task-struct.md +29 -0
  36. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T002-frontmatter-parser.md +30 -0
  37. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T003-router-reader.md +29 -0
  38. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T004-config-reader.md +29 -0
  39. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T005-discovery.md +30 -0
  40. package/.savepoint/releases/v1/epics/E03-board-tui-core/Design.md +38 -0
  41. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T001-model.md +29 -0
  42. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T002-update-loop.md +30 -0
  43. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T003-view.md +34 -0
  44. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T004-styles.md +29 -0
  45. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +42 -0
  46. package/.savepoint/releases/v1/epics/E04-board-components/Design.md +44 -0
  47. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T001-column.md +34 -0
  48. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +33 -0
  49. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T003-epic-panel.md +49 -0
  50. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T004-detail-overlay.md +40 -0
  51. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T005-release-dropdown.md +33 -0
  52. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +34 -0
  53. package/.savepoint/releases/v1/epics/E05-phase-transitions/Design.md +38 -0
  54. package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T001-phase-stepping.md +29 -0
  55. package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T002-gates.md +31 -0
  56. package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T003-write-task.md +31 -0
  57. package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T004-write-router.md +31 -0
  58. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/Design.md +42 -0
  59. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T001-color-system.md +39 -0
  60. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +52 -0
  61. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +52 -0
  62. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +53 -0
  63. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T005-restore-nav-hints.md +39 -0
  64. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T007-detail-card-fixes.md +36 -0
  65. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T008-checkbox-states.md +38 -0
  66. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T009-router-priority-marker.md +41 -0
  67. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +61 -0
  68. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/Design.md +39 -0
  69. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T001-archive-epics.md +20 -0
  70. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T002-rewrite-prd.md +22 -0
  71. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T003-create-epic-stubs.md +24 -0
  72. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T004-update-router.md +22 -0
  73. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/Design.md +118 -0
  74. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/handoff.md +9 -0
  75. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T001-package-baseline.md +45 -0
  76. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T002-typescript-build.md +48 -0
  77. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T003-vitest-smoke.md +43 -0
  78. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T004-lint-format-gates.md +45 -0
  79. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T005-scaffold-verification.md +40 -0
  80. package/.savepoint/releases/v1/epics/_archived/E02-data-model/Design.md +142 -0
  81. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T001-domain-ids-status.md +27 -0
  82. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T002-markdown-frontmatter-boundary.md +28 -0
  83. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T003-task-documents.md +29 -0
  84. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T004-release-epic-router-config-readers.md +30 -0
  85. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T005-dependency-validation.md +29 -0
  86. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T006-epic-task-set-reader.md +29 -0
  87. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T007-quality-gates.md +31 -0
  88. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/Design.md +40 -0
  89. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T001-phase-types.md +27 -0
  90. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T002-phase-frontmatter.md +25 -0
  91. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T003-simplify-config.md +26 -0
  92. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T004-simplify-router-domain.md +24 -0
  93. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/Design.md +122 -0
  94. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T001-argument-parser-contract.md +28 -0
  95. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T002-help-text-generation.md +28 -0
  96. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T003-terminal-environment-detection.md +27 -0
  97. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T004-command-stub-modules.md +29 -0
  98. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T005-cli-runner-dispatch.md +34 -0
  99. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T006-entrypoint-quality-gates.md +32 -0
  100. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/Design.md +43 -0
  101. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T001-strip-args.md +26 -0
  102. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T002-strip-help.md +23 -0
  103. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T003-strip-run.md +23 -0
  104. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T004-delete-commands.md +24 -0
  105. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T005-update-cli-tests.md +22 -0
  106. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/Design.md +48 -0
  107. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T001-board-data-phases.md +26 -0
  108. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T002-phase-rendering.md +28 -0
  109. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T003-detail-pane-phases.md +27 -0
  110. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T004-phase-transitions.md +42 -0
  111. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T005-phase-gates.md +24 -0
  112. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T006-phase-write-back.md +24 -0
  113. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T007-remove-audit-flow.md +27 -0
  114. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T008-board-tests.md +25 -0
  115. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/Design.md +85 -0
  116. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T001-project-template-assets.md +17 -0
  117. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T002-release-and-prompt-assets.md +20 -0
  118. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T003-template-registry-renderer.md +22 -0
  119. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T004-template-integrity-tests.md +17 -0
  120. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T005-template-closeout-quality-gates.md +16 -0
  121. package/.savepoint/releases/v1/epics/_archived/E05-init-command/Design.md +88 -0
  122. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T001-init-cli-contract.md +22 -0
  123. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T002-target-validation.md +23 -0
  124. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T003-scaffold-writer.md +24 -0
  125. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T004-magic-prompt-and-clipboard.md +23 -0
  126. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T005-dev-deps-install-option.md +24 -0
  127. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T006-init-command-integration.md +28 -0
  128. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/Design.md +53 -0
  129. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T001-delete-dead-src.md +23 -0
  130. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T002-delete-dead-tests.md +26 -0
  131. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T003-delete-assets.md +25 -0
  132. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T004-clean-savepoint.md +28 -0
  133. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T005-rewrite-agents-md.md +28 -0
  134. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T006-clean-package-json.md +23 -0
  135. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T007-verify.md +25 -0
  136. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/Design.md +104 -0
  137. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T001-board-command-data.md +23 -0
  138. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T002-board-view-state.md +24 -0
  139. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T003-transition-gates-and-writes.md +25 -0
  140. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T004-terminal-theme.md +23 -0
  141. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T005-ink-board-ui.md +26 -0
  142. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T006-board-integration-audit-entry.md +24 -0
  143. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/Design.md +88 -0
  144. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T001-audit-cli-contract.md +23 -0
  145. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T002-quality-gate-runner.md +23 -0
  146. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T003-snapshot-and-prompt.md +23 -0
  147. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T004-audit-orchestration-router.md +27 -0
  148. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T005-proposal-validation-apply.md +25 -0
  149. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T006-audit-review-state.md +24 -0
  150. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T007-audit-review-ui.md +26 -0
  151. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T008-audit-pipeline-integration.md +24 -0
  152. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/Design.md +103 -0
  153. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T001-acceptance-criteria-model.md +30 -0
  154. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T002-release-task-set-reader.md +33 -0
  155. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T003-board-data-and-plain-output.md +34 -0
  156. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T004-board-selection-state.md +33 -0
  157. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T005-ink-board-layout-cleanup.md +37 -0
  158. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T006-task-detail-popup.md +36 -0
  159. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T007-templates-acceptance-criteria.md +34 -0
  160. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T008-board-workflow-integration.md +41 -0
  161. package/.savepoint/releases/v1/epics/_archived/E09-doctor-command/Design.md +70 -0
  162. package/.savepoint/releases/v1/epics/_archived/E10-docs-and-packaging/Design.md +68 -0
  163. package/.savepoint/releases/v1/epics/_archived/E11-release-validation/Design.md +68 -0
  164. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/Design.md +26 -0
  165. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +35 -0
  166. package/.savepoint/router.md +136 -0
  167. package/.savepoint/visual-identity.md +124 -0
  168. package/AGENTS.md +141 -0
  169. package/CLAUDE.md +1 -0
  170. package/GEMINI.md +1 -0
  171. package/LICENSE +21 -0
  172. package/Makefile +13 -0
  173. package/README.md +78 -0
  174. package/agent-skills/ink-tui-design/SKILL.md +309 -0
  175. package/agent-skills/ink-tui-design/references/component-patterns.md +371 -0
  176. package/agent-skills/ink-tui-design/references/hooks-guide.md +436 -0
  177. package/agent-skills/ink-tui-design/references/ink-gotchas.md +330 -0
  178. package/agent-skills/ink-tui-design/references/testing-patterns.md +384 -0
  179. package/agent-skills/savepoint-audit/SKILL.md +35 -0
  180. package/agent-skills/savepoint-build-task/SKILL.md +39 -0
  181. package/agent-skills/savepoint-create-plan/SKILL.md +28 -0
  182. package/agent-skills/savepoint-create-task/SKILL.md +31 -0
  183. package/agent-skills/savepoint-draft-prd/SKILL.md +32 -0
  184. package/agent-skills/savepoint-system-design/SKILL.md +33 -0
  185. package/agent-skills/superpowers/brainstorming/SKILL.md +165 -0
  186. package/agent-skills/superpowers/brainstorming/visual-companion.md +304 -0
  187. package/agent-skills/superpowers/dispatching-parallel-agents/SKILL.md +193 -0
  188. package/agent-skills/superpowers/executing-plans/SKILL.md +77 -0
  189. package/agent-skills/superpowers/finishing-a-development-branch/SKILL.md +213 -0
  190. package/agent-skills/superpowers/receiving-code-review/SKILL.md +226 -0
  191. package/agent-skills/superpowers/requesting-code-review/SKILL.md +115 -0
  192. package/agent-skills/superpowers/requesting-code-review/code-reviewer.md +160 -0
  193. package/agent-skills/superpowers/subagent-driven-development/SKILL.md +292 -0
  194. package/agent-skills/superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +27 -0
  195. package/agent-skills/superpowers/subagent-driven-development/implementer-prompt.md +113 -0
  196. package/agent-skills/superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
  197. package/agent-skills/superpowers/systematic-debugging/SKILL.md +305 -0
  198. package/agent-skills/superpowers/systematic-debugging/condition-based-waiting.md +122 -0
  199. package/agent-skills/superpowers/systematic-debugging/defense-in-depth.md +130 -0
  200. package/agent-skills/superpowers/systematic-debugging/root-cause-tracing.md +183 -0
  201. package/agent-skills/superpowers/test-driven-development/SKILL.md +389 -0
  202. package/agent-skills/superpowers/test-driven-development/testing-anti-patterns.md +317 -0
  203. package/agent-skills/superpowers/verification-before-completion/SKILL.md +147 -0
  204. package/agent-skills/superpowers/writing-plans/SKILL.md +159 -0
  205. package/agent-skills/superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
  206. package/assets/banner.png +0 -0
  207. package/assets/logo.png +0 -0
  208. package/assets/strawman.png +0 -0
  209. package/go.mod +33 -0
  210. package/go.sum +73 -0
  211. package/ink-cli-ui-design.zip +0 -0
  212. package/internal/board/board.go +121 -0
  213. package/internal/board/board_test.go +99 -0
  214. package/internal/board/card.go +72 -0
  215. package/internal/board/card_test.go +111 -0
  216. package/internal/board/column.go +61 -0
  217. package/internal/board/column_test.go +81 -0
  218. package/internal/board/detail.go +140 -0
  219. package/internal/board/detail_test.go +233 -0
  220. package/internal/board/epic_panel.go +69 -0
  221. package/internal/board/epic_panel_test.go +246 -0
  222. package/internal/board/help.go +40 -0
  223. package/internal/board/help_test.go +85 -0
  224. package/internal/board/layout.go +58 -0
  225. package/internal/board/layout_test.go +89 -0
  226. package/internal/board/model.go +151 -0
  227. package/internal/board/model_test.go +67 -0
  228. package/internal/board/release.go +42 -0
  229. package/internal/board/release_test.go +177 -0
  230. package/internal/board/transitions.go +88 -0
  231. package/internal/board/transitions_test.go +141 -0
  232. package/internal/board/update.go +155 -0
  233. package/internal/board/update_test.go +128 -0
  234. package/internal/board/view.go +190 -0
  235. package/internal/board/view_test.go +147 -0
  236. package/internal/data/config.go +87 -0
  237. package/internal/data/config_test.go +73 -0
  238. package/internal/data/discover.go +152 -0
  239. package/internal/data/discover_test.go +106 -0
  240. package/internal/data/errors.go +9 -0
  241. package/internal/data/lifecycle.go +37 -0
  242. package/internal/data/lifecycle_test.go +38 -0
  243. package/internal/data/parser.go +189 -0
  244. package/internal/data/parser_test.go +216 -0
  245. package/internal/data/router.go +52 -0
  246. package/internal/data/router_test.go +35 -0
  247. package/internal/data/task.go +46 -0
  248. package/internal/data/task_test.go +51 -0
  249. package/internal/data/write.go +144 -0
  250. package/internal/data/write_test.go +456 -0
  251. package/internal/styles/palette.go +47 -0
  252. package/internal/styles/styles.go +122 -0
  253. package/main.exe +0 -0
  254. package/main.go +11 -0
  255. package/package.json +25 -0
  256. package/savepoint +0 -0
  257. package/savepoint.exe +0 -0
  258. package/scripts/vitest-preload.cjs +95 -0
  259. package/templates/project/.savepoint/Design.md +47 -0
  260. package/templates/project/.savepoint/PRD.md +34 -0
  261. package/templates/project/.savepoint/config.yml +27 -0
  262. package/templates/project/.savepoint/router.md +152 -0
  263. package/templates/project/.savepoint/visual-identity.md +122 -0
  264. package/templates/project/AGENTS.md +130 -0
  265. package/templates/prompts/audit-reconciliation.prompt.md +67 -0
  266. package/templates/prompts/design.prompt.md +43 -0
  267. package/templates/prompts/epic-design.prompt.md +43 -0
  268. package/templates/prompts/magic-prompt.prompt.md +7 -0
  269. package/templates/prompts/prd.prompt.md +42 -0
  270. package/templates/prompts/task-breakdown.prompt.md +54 -0
  271. package/templates/prompts/task-building.prompt.md +38 -0
  272. package/templates/prompts/task-planning.prompt.md +53 -0
  273. package/templates/release/v1/PRD.md +37 -0
@@ -0,0 +1,147 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+
7
+ "github.com/charmbracelet/lipgloss"
8
+ "github.com/opencode/savepoint/internal/data"
9
+ )
10
+
11
+ func TestView_rendersWithoutPanic(t *testing.T) {
12
+ m := NewModel(nil, "v1", "E03")
13
+ m.Width = 120
14
+ m.Height = 40
15
+ got := m.View()
16
+ if got == "" {
17
+ t.Error("View() returned empty string")
18
+ }
19
+ }
20
+
21
+ func TestView_containsColumnTitles(t *testing.T) {
22
+ m := NewModel(nil, "v1", "E03")
23
+ m.Width = 120
24
+ got := m.View()
25
+
26
+ for _, title := range []string{"PLANNED", "IN PROGRESS", "DONE"} {
27
+ if !strings.Contains(got, title) {
28
+ t.Errorf("View() missing column title %q", title)
29
+ }
30
+ }
31
+ }
32
+
33
+ func TestView_containsHeader(t *testing.T) {
34
+ m := NewModel(nil, "v1", "E03")
35
+ m.Width = 120
36
+ got := m.View()
37
+ if !strings.Contains(got, "S A V E P O I N T") {
38
+ t.Error("View() missing spaced header text")
39
+ }
40
+ if !strings.Contains(got, "▣") {
41
+ t.Error("View() missing header icon")
42
+ }
43
+ }
44
+
45
+ func TestView_containsDivider(t *testing.T) {
46
+ m := NewModel(nil, "v1", "E03")
47
+ m.Width = 120
48
+ got := m.View()
49
+ if !strings.Contains(got, "─") {
50
+ t.Error("View() missing horizontal divider")
51
+ }
52
+ }
53
+
54
+ func TestView_containsFooterPhases(t *testing.T) {
55
+ m := NewModel(nil, "v1", "E03")
56
+ m.Width = 120
57
+ got := m.View()
58
+ for _, phase := range []string{"PLAN", "BUILD", "AUDIT"} {
59
+ if !strings.Contains(got, phase) {
60
+ t.Errorf("View() missing footer phase %q", phase)
61
+ }
62
+ }
63
+ }
64
+
65
+ func TestView_containsFooterHints(t *testing.T) {
66
+ m := NewModel(nil, "v1", "E03")
67
+ footer := m.renderFooter(80)
68
+
69
+ if !strings.Contains(footer, "←/→:nav E:epic R:release ?:help q:quit") {
70
+ t.Fatal("renderFooter() missing navigation hints")
71
+ }
72
+
73
+ lines := strings.Split(footer, "\n")
74
+ if len(lines) != 3 {
75
+ t.Fatalf("renderFooter() returned %d lines, want 3", len(lines))
76
+ }
77
+ if strings.TrimSpace(lines[1]) != "" {
78
+ t.Fatalf("renderFooter() spacer line = %q, want blank", lines[1])
79
+ }
80
+ for i, line := range lines {
81
+ if got := lipgloss.Width(line); got > 80 {
82
+ t.Fatalf("renderFooter() line %d width = %d, want <= 80", i, got)
83
+ }
84
+ }
85
+ }
86
+
87
+ func TestView_containsBottomDivider(t *testing.T) {
88
+ m := NewModel(nil, "v1", "E03")
89
+ m.Width = 120
90
+ got := m.View()
91
+ // There should be at least two divider lines (top and bottom)
92
+ count := strings.Count(got, "─")
93
+ if count < 2 {
94
+ t.Errorf("View() expected at least 2 divider chars, got %d", count)
95
+ }
96
+ }
97
+
98
+ func TestView_defaultWidthWhenZero(t *testing.T) {
99
+ m := NewModel(nil, "v1", "E03")
100
+ // Width=0: should use default and not panic
101
+ got := m.View()
102
+ if got == "" {
103
+ t.Error("View() with zero Width returned empty string")
104
+ }
105
+ }
106
+
107
+ func TestView_taskLabelFallback(t *testing.T) {
108
+ tasks := []data.Task{{ID: "T1", Title: "", Column: data.ColumnPlanned}}
109
+ m := NewModel(tasks, "v1", "E03")
110
+ m.Width = 120
111
+ got := m.View()
112
+ if !strings.Contains(got, "T1") {
113
+ t.Error("View() missing task ID when title is empty")
114
+ }
115
+ }
116
+
117
+ func TestView_taskLabelWithTitle(t *testing.T) {
118
+ tasks := []data.Task{{ID: "T2", Title: "My Task", Column: data.ColumnPlanned}}
119
+ m := NewModel(tasks, "v1", "E03")
120
+ m.Width = 120
121
+ got := m.View()
122
+ if !strings.Contains(got, "My Task") {
123
+ t.Error("View() missing task title")
124
+ }
125
+ }
126
+
127
+ func TestView_wideShowsEpicPanel(t *testing.T) {
128
+ m := NewModel(nil, "v1", "E03")
129
+ m.Width = 120
130
+ got := m.View()
131
+ if !strings.Contains(got, "E03") {
132
+ t.Error("View() at width>=120 missing epic panel content")
133
+ }
134
+ }
135
+
136
+ func TestView_narrowShowsSingleColumn(t *testing.T) {
137
+ m := NewModel(nil, "v1", "E03")
138
+ m.Width = 60
139
+ m.FocusedColumn = data.ColumnInProgress
140
+ got := m.View()
141
+ if !strings.Contains(got, "IN PROGRESS") {
142
+ t.Error("View() at width<80 missing focused column title")
143
+ }
144
+ if strings.Contains(got, "PLANNED") {
145
+ t.Error("View() at width<80 should not show non-focused columns")
146
+ }
147
+ }
@@ -0,0 +1,87 @@
1
+ package data
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+
7
+ "gopkg.in/yaml.v3"
8
+ )
9
+
10
+ type Theme struct {
11
+ BG string `yaml:"bg"`
12
+ Surface string `yaml:"surface"`
13
+ Surface2 string `yaml:"surface_2"`
14
+ Border string `yaml:"border"`
15
+ Text string `yaml:"text"`
16
+ Accents map[string]string `yaml:"accents"`
17
+ }
18
+
19
+ type Config struct {
20
+ Theme Theme `yaml:"theme"`
21
+ }
22
+
23
+ var defaultTheme = Theme{
24
+ BG: "#1a1b26",
25
+ Surface: "#24283b",
26
+ Surface2: "#414868",
27
+ Border: "#565f89",
28
+ Text: "#c0caf5",
29
+ Accents: map[string]string{
30
+ "planned": "#7aa2f7",
31
+ "in_progress": "#bb9af7",
32
+ "done": "#9ece6a",
33
+ "blocked": "#f7768e",
34
+ "epic": "#2ac3de",
35
+ },
36
+ }
37
+
38
+ var defaultConfig = Config{
39
+ Theme: defaultTheme,
40
+ }
41
+
42
+ type ConfigReader struct{}
43
+
44
+ func NewConfigReader() *ConfigReader {
45
+ return &ConfigReader{}
46
+ }
47
+
48
+ func (r *ConfigReader) Read(path string) (*Config, error) {
49
+ data, err := os.ReadFile(path)
50
+ if os.IsNotExist(err) {
51
+ return &defaultConfig, nil
52
+ }
53
+ if err != nil {
54
+ return nil, fmt.Errorf("failed to read config: %w", err)
55
+ }
56
+
57
+ var config Config
58
+ if err := yaml.Unmarshal(data, &config); err != nil {
59
+ return nil, fmt.Errorf("failed to parse config YAML: %w", err)
60
+ }
61
+
62
+ config.Theme = fillThemeDefaults(config.Theme)
63
+
64
+ return &config, nil
65
+ }
66
+
67
+ func fillThemeDefaults(theme Theme) Theme {
68
+ if theme.BG == "" {
69
+ theme.BG = defaultTheme.BG
70
+ }
71
+ if theme.Surface == "" {
72
+ theme.Surface = defaultTheme.Surface
73
+ }
74
+ if theme.Surface2 == "" {
75
+ theme.Surface2 = defaultTheme.Surface2
76
+ }
77
+ if theme.Border == "" {
78
+ theme.Border = defaultTheme.Border
79
+ }
80
+ if theme.Text == "" {
81
+ theme.Text = defaultTheme.Text
82
+ }
83
+ if len(theme.Accents) == 0 {
84
+ theme.Accents = defaultTheme.Accents
85
+ }
86
+ return theme
87
+ }
@@ -0,0 +1,73 @@
1
+ package data
2
+
3
+ import (
4
+ "os"
5
+ "testing"
6
+ )
7
+
8
+ func TestConfigReaderDefault(t *testing.T) {
9
+ r := NewConfigReader()
10
+ config, err := r.Read("nonexistent.yml")
11
+ if err != nil {
12
+ t.Fatalf("Read() error = %v", err)
13
+ }
14
+
15
+ if config.Theme.BG != defaultTheme.BG {
16
+ t.Errorf("Theme.BG = %v, want %v", config.Theme.BG, defaultTheme.BG)
17
+ }
18
+ }
19
+
20
+ func TestConfigReaderRead(t *testing.T) {
21
+ content := `theme:
22
+ bg: "#000000"
23
+ surface: "#111111"
24
+ text: "#ffffff"
25
+ accents:
26
+ planned: "#222222"
27
+ `
28
+ tmpfile, err := os.CreateTemp("", "config-*.yml")
29
+ if err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ defer os.Remove(tmpfile.Name())
33
+
34
+ if _, err := tmpfile.Write([]byte(content)); err != nil {
35
+ t.Fatal(err)
36
+ }
37
+ tmpfile.Close()
38
+
39
+ r := NewConfigReader()
40
+ config, err := r.Read(tmpfile.Name())
41
+ if err != nil {
42
+ t.Fatalf("Read() error = %v", err)
43
+ }
44
+
45
+ if config.Theme.BG != "#000000" {
46
+ t.Errorf("Theme.BG = %v, want #000000", config.Theme.BG)
47
+ }
48
+ if config.Theme.Surface2 != defaultTheme.Surface2 {
49
+ t.Errorf("Theme.Surface2 = %v, want default %v", config.Theme.Surface2, defaultTheme.Surface2)
50
+ }
51
+ if config.Theme.Accents["planned"] != "#222222" {
52
+ t.Errorf("Theme.Accents[planned] = %v, want #222222", config.Theme.Accents["planned"])
53
+ }
54
+ }
55
+
56
+ func TestConfigReaderMalformedYAML(t *testing.T) {
57
+ tmpfile, err := os.CreateTemp("", "config-*.yml")
58
+ if err != nil {
59
+ t.Fatal(err)
60
+ }
61
+ defer os.Remove(tmpfile.Name())
62
+
63
+ if _, err := tmpfile.Write([]byte("theme: [broken")); err != nil {
64
+ t.Fatal(err)
65
+ }
66
+ tmpfile.Close()
67
+
68
+ r := NewConfigReader()
69
+ _, err = r.Read(tmpfile.Name())
70
+ if err == nil {
71
+ t.Fatal("Read() expected malformed YAML error")
72
+ }
73
+ }
@@ -0,0 +1,152 @@
1
+ package data
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "path/filepath"
7
+ "sort"
8
+ "strings"
9
+ )
10
+
11
+ type ReleaseInfo struct {
12
+ ID string
13
+ Path string
14
+ Epics []EpicInfo
15
+ }
16
+
17
+ type EpicInfo struct {
18
+ ID string
19
+ Path string
20
+ Tasks []TaskInfo
21
+ }
22
+
23
+ type TaskInfo struct {
24
+ ID string
25
+ Path string
26
+ }
27
+
28
+ type Discover struct{}
29
+
30
+ func NewDiscover() *Discover {
31
+ return &Discover{}
32
+ }
33
+
34
+ func (d *Discover) FindSavepointRoot(start string) (string, error) {
35
+ dir, err := filepath.Abs(start)
36
+ if err != nil {
37
+ return "", err
38
+ }
39
+
40
+ for {
41
+ savepointPath := filepath.Join(dir, ".savepoint")
42
+ info, err := os.Stat(savepointPath)
43
+ if err == nil && info.IsDir() {
44
+ return savepointPath, nil
45
+ }
46
+
47
+ parent := filepath.Dir(dir)
48
+ if parent == dir {
49
+ return "", ErrSavepointDirectoryMissing
50
+ }
51
+ dir = parent
52
+ }
53
+ }
54
+
55
+ func (d *Discover) ListReleases(root string) ([]ReleaseInfo, error) {
56
+ releasesPath := filepath.Join(root, "releases")
57
+ info, err := os.Stat(releasesPath)
58
+ if err != nil {
59
+ return nil, fmt.Errorf("releases directory not found: %w", err)
60
+ }
61
+ if !info.IsDir() {
62
+ return nil, fmt.Errorf("releases is not a directory")
63
+ }
64
+
65
+ entries, err := os.ReadDir(releasesPath)
66
+ if err != nil {
67
+ return nil, err
68
+ }
69
+
70
+ var releases []ReleaseInfo
71
+ for _, entry := range entries {
72
+ if !entry.IsDir() {
73
+ continue
74
+ }
75
+ id := entry.Name()
76
+ releases = append(releases, ReleaseInfo{
77
+ ID: id,
78
+ Path: filepath.Join(releasesPath, id),
79
+ })
80
+ }
81
+
82
+ sort.Slice(releases, func(i, j int) bool {
83
+ return releases[i].ID < releases[j].ID
84
+ })
85
+ return releases, nil
86
+ }
87
+
88
+ func (d *Discover) ListEpics(root, release string) ([]EpicInfo, error) {
89
+ epicsPath := filepath.Join(root, "releases", release, "epics")
90
+ info, err := os.Stat(epicsPath)
91
+ if err != nil {
92
+ return nil, fmt.Errorf("epics directory not found: %w", err)
93
+ }
94
+ if !info.IsDir() {
95
+ return nil, fmt.Errorf("epics is not a directory")
96
+ }
97
+
98
+ entries, err := os.ReadDir(epicsPath)
99
+ if err != nil {
100
+ return nil, err
101
+ }
102
+
103
+ var epics []EpicInfo
104
+ for _, entry := range entries {
105
+ if !entry.IsDir() || strings.HasPrefix(entry.Name(), "_") {
106
+ continue
107
+ }
108
+ id := entry.Name()
109
+ epics = append(epics, EpicInfo{
110
+ ID: id,
111
+ Path: filepath.Join(epicsPath, id),
112
+ })
113
+ }
114
+
115
+ sort.Slice(epics, func(i, j int) bool {
116
+ return epics[i].ID < epics[j].ID
117
+ })
118
+ return epics, nil
119
+ }
120
+
121
+ func (d *Discover) ListTasks(root, release, epic string) ([]TaskInfo, error) {
122
+ tasksPath := filepath.Join(root, "releases", release, "epics", epic, "tasks")
123
+ info, err := os.Stat(tasksPath)
124
+ if err != nil {
125
+ return nil, fmt.Errorf("tasks directory not found: %w", err)
126
+ }
127
+ if !info.IsDir() {
128
+ return nil, fmt.Errorf("tasks is not a directory")
129
+ }
130
+
131
+ entries, err := os.ReadDir(tasksPath)
132
+ if err != nil {
133
+ return nil, err
134
+ }
135
+
136
+ var tasks []TaskInfo
137
+ for _, entry := range entries {
138
+ if entry.IsDir() || filepath.Ext(entry.Name()) != ".md" {
139
+ continue
140
+ }
141
+ id := entry.Name()[:len(entry.Name())-3]
142
+ tasks = append(tasks, TaskInfo{
143
+ ID: id,
144
+ Path: filepath.Join(tasksPath, entry.Name()),
145
+ })
146
+ }
147
+
148
+ sort.Slice(tasks, func(i, j int) bool {
149
+ return tasks[i].ID < tasks[j].ID
150
+ })
151
+ return tasks, nil
152
+ }
@@ -0,0 +1,106 @@
1
+ package data
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+ )
8
+
9
+ func TestFindSavepointRoot(t *testing.T) {
10
+ d := NewDiscover()
11
+ savepointRoot := createDiscoveryFixture(t)
12
+ start := filepath.Join(filepath.Dir(savepointRoot), "nested", "child")
13
+ if err := os.MkdirAll(start, 0755); err != nil {
14
+ t.Fatal(err)
15
+ }
16
+
17
+ root, err := d.FindSavepointRoot(start)
18
+ if err != nil {
19
+ t.Fatalf("FindSavepointRoot() error = %v", err)
20
+ }
21
+ if root != savepointRoot {
22
+ t.Errorf("FindSavepointRoot() = %v, want %v", root, savepointRoot)
23
+ }
24
+ }
25
+
26
+ func TestListReleases(t *testing.T) {
27
+ d := NewDiscover()
28
+ root := createDiscoveryFixture(t)
29
+
30
+ releases, err := d.ListReleases(root)
31
+ if err != nil {
32
+ t.Fatalf("ListReleases() error = %v", err)
33
+ }
34
+
35
+ if len(releases) != 2 {
36
+ t.Fatalf("ListReleases() returned %d releases, want 2", len(releases))
37
+ }
38
+ if releases[0].ID != "v1" || releases[1].ID != "v2" {
39
+ t.Errorf("ListReleases() IDs = %v, want [v1 v2]", []string{releases[0].ID, releases[1].ID})
40
+ }
41
+ }
42
+
43
+ func TestListEpics(t *testing.T) {
44
+ d := NewDiscover()
45
+ root := createDiscoveryFixture(t)
46
+
47
+ epics, err := d.ListEpics(root, "v1")
48
+ if err != nil {
49
+ t.Fatalf("ListEpics() error = %v", err)
50
+ }
51
+
52
+ if len(epics) != 2 {
53
+ t.Fatalf("ListEpics() returned %d epics, want 2", len(epics))
54
+ }
55
+ if epics[0].ID != "E01-go-setup" || epics[1].ID != "E02-data-readers" {
56
+ t.Errorf("ListEpics() IDs = %v, want [E01-go-setup E02-data-readers]", []string{epics[0].ID, epics[1].ID})
57
+ }
58
+ }
59
+
60
+ func TestListTasks(t *testing.T) {
61
+ d := NewDiscover()
62
+ root := createDiscoveryFixture(t)
63
+
64
+ tasks, err := d.ListTasks(root, "v1", "E02-data-readers")
65
+ if err != nil {
66
+ t.Fatalf("ListTasks() error = %v", err)
67
+ }
68
+
69
+ if len(tasks) != 2 {
70
+ t.Fatalf("ListTasks() returned %d tasks, want 2", len(tasks))
71
+ }
72
+ if tasks[0].ID != "T001-task-struct" || tasks[1].ID != "T002-frontmatter-parser" {
73
+ t.Errorf("ListTasks() IDs = %v, want [T001-task-struct T002-frontmatter-parser]", []string{tasks[0].ID, tasks[1].ID})
74
+ }
75
+ }
76
+
77
+ func createDiscoveryFixture(t *testing.T) string {
78
+ t.Helper()
79
+
80
+ root := t.TempDir()
81
+ savepointRoot := filepath.Join(root, ".savepoint")
82
+ paths := []string{
83
+ filepath.Join(savepointRoot, "releases", "v1", "epics", "E02-data-readers", "tasks"),
84
+ filepath.Join(savepointRoot, "releases", "v1", "epics", "E01-go-setup", "tasks"),
85
+ filepath.Join(savepointRoot, "releases", "v1", "epics", "_archived"),
86
+ filepath.Join(savepointRoot, "releases", "v2", "epics"),
87
+ }
88
+ for _, path := range paths {
89
+ if err := os.MkdirAll(path, 0755); err != nil {
90
+ t.Fatal(err)
91
+ }
92
+ }
93
+
94
+ files := []string{
95
+ filepath.Join(savepointRoot, "releases", "v1", "epics", "E02-data-readers", "tasks", "T002-frontmatter-parser.md"),
96
+ filepath.Join(savepointRoot, "releases", "v1", "epics", "E02-data-readers", "tasks", "T001-task-struct.md"),
97
+ filepath.Join(savepointRoot, "releases", "v1", "epics", "E02-data-readers", "tasks", "notes.txt"),
98
+ }
99
+ for _, file := range files {
100
+ if err := os.WriteFile(file, []byte("test"), 0644); err != nil {
101
+ t.Fatal(err)
102
+ }
103
+ }
104
+
105
+ return savepointRoot
106
+ }
@@ -0,0 +1,9 @@
1
+ package data
2
+
3
+ import "errors"
4
+
5
+ var (
6
+ ErrNoFrontmatter = errors.New("no frontmatter found")
7
+ ErrNoClosingFrontmatter = errors.New("no closing frontmatter delimiter found")
8
+ ErrSavepointDirectoryMissing = errors.New(".savepoint directory not found")
9
+ )
@@ -0,0 +1,37 @@
1
+ package data
2
+
3
+ import "fmt"
4
+
5
+ func ValidateTaskLifecycle(task Task) error {
6
+ if !IsCanonicalColumn(task.Column) {
7
+ return fmt.Errorf("invalid task status %q: use planned, in_progress, or done", task.Column)
8
+ }
9
+
10
+ if task.Column != ColumnInProgress && task.Stage != "" {
11
+ return fmt.Errorf("phase %q is only valid when status is in_progress", task.Stage)
12
+ }
13
+
14
+ if task.Column == ColumnInProgress && !IsCanonicalStage(task.Stage) {
15
+ return fmt.Errorf("invalid in_progress phase %q: use build, test, or audit", task.Stage)
16
+ }
17
+
18
+ return nil
19
+ }
20
+
21
+ func IsCanonicalColumn(value ColumnType) bool {
22
+ switch value {
23
+ case ColumnPlanned, ColumnInProgress, ColumnDone:
24
+ return true
25
+ default:
26
+ return false
27
+ }
28
+ }
29
+
30
+ func IsCanonicalStage(value ProgressStage) bool {
31
+ switch value {
32
+ case StageBuild, StageTest, StageAudit:
33
+ return true
34
+ default:
35
+ return false
36
+ }
37
+ }
@@ -0,0 +1,38 @@
1
+ package data
2
+
3
+ import "testing"
4
+
5
+ func TestValidateTaskLifecycle_allowsPlannedWithoutPhase(t *testing.T) {
6
+ task := Task{Column: ColumnPlanned}
7
+ if err := ValidateTaskLifecycle(task); err != nil {
8
+ t.Fatalf("ValidateTaskLifecycle() error = %v", err)
9
+ }
10
+ }
11
+
12
+ func TestValidateTaskLifecycle_allowsInProgressWithPhase(t *testing.T) {
13
+ task := Task{Column: ColumnInProgress, Stage: StageAudit}
14
+ if err := ValidateTaskLifecycle(task); err != nil {
15
+ t.Fatalf("ValidateTaskLifecycle() error = %v", err)
16
+ }
17
+ }
18
+
19
+ func TestValidateTaskLifecycle_rejectsUnknownStatus(t *testing.T) {
20
+ task := Task{Column: "review"}
21
+ if err := ValidateTaskLifecycle(task); err == nil {
22
+ t.Fatal("ValidateTaskLifecycle() expected unknown status error")
23
+ }
24
+ }
25
+
26
+ func TestValidateTaskLifecycle_rejectsPhaseOutsideInProgress(t *testing.T) {
27
+ task := Task{Column: ColumnPlanned, Stage: StageBuild}
28
+ if err := ValidateTaskLifecycle(task); err == nil {
29
+ t.Fatal("ValidateTaskLifecycle() expected phase/status error")
30
+ }
31
+ }
32
+
33
+ func TestValidateTaskLifecycle_rejectsInProgressWithoutCanonicalPhase(t *testing.T) {
34
+ task := Task{Column: ColumnInProgress}
35
+ if err := ValidateTaskLifecycle(task); err == nil {
36
+ t.Fatal("ValidateTaskLifecycle() expected missing phase error")
37
+ }
38
+ }