savepoint 1.0.2 → 1.0.3

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 (233) hide show
  1. package/.claude/settings.local.json +12 -1
  2. package/.golangci.yml +11 -0
  3. package/.savepoint/Design.md +37 -36
  4. package/.savepoint/{audit/v1.1/E02-cross-platform-compatibility/proposals.md → releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md} +48 -38
  5. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
  6. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +14 -1
  7. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +3 -3
  8. package/.savepoint/{audit/v1.1/E04-epic-navigation/proposals.md → releases/v1.1/epics/E04-epic-navigation/E04-Audit.md} +65 -54
  9. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
  10. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +25 -16
  11. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +17 -6
  12. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +15 -5
  13. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +19 -5
  14. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +11 -1
  15. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +9 -6
  16. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +29 -13
  17. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
  18. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
  19. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
  20. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
  21. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
  22. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
  23. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
  24. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
  25. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
  26. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
  27. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
  28. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
  29. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
  30. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
  31. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
  32. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
  33. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
  34. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
  35. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
  36. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
  37. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
  38. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
  39. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
  40. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
  41. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
  42. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
  43. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
  44. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
  45. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
  46. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
  47. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
  48. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
  49. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
  50. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
  51. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
  52. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
  53. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
  54. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
  55. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
  56. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
  57. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
  58. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
  59. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
  60. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
  61. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
  62. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
  63. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
  64. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
  65. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
  66. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
  67. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
  68. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
  69. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
  70. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
  71. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
  72. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
  73. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
  74. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
  75. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
  76. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
  77. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
  78. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
  79. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
  80. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
  81. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
  82. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
  83. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
  84. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +43 -0
  85. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
  86. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +28 -0
  87. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
  88. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
  89. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
  90. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
  91. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +28 -0
  92. package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
  93. package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
  94. package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
  95. package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
  96. package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
  97. package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
  98. package/.savepoint/releases/v1.1/v1.1-PRD.md +67 -7
  99. package/.savepoint/router.md +9 -16
  100. package/AGENTS.md +38 -23
  101. package/README.md +0 -1
  102. package/agent-skills/savepoint-audit/SKILL.md +86 -34
  103. package/agent-skills/savepoint-build-task/SKILL.md +7 -2
  104. package/agent-skills/savepoint-create-plan/SKILL.md +7 -2
  105. package/agent-skills/savepoint-create-task/SKILL.md +44 -31
  106. package/agent-skills/savepoint-draft-prd/SKILL.md +7 -2
  107. package/agent-skills/savepoint-system-design/SKILL.md +7 -2
  108. package/agent_skills_test.go +91 -0
  109. package/cmd/board.go +59 -0
  110. package/cmd/board_test.go +137 -0
  111. package/cmd/doctor.go +53 -0
  112. package/cmd/doctor_test.go +146 -0
  113. package/cmd/init.go +63 -0
  114. package/cmd/init_test.go +104 -0
  115. package/internal/board/board.go +40 -36
  116. package/internal/board/board_test.go +27 -82
  117. package/internal/board/card.go +43 -23
  118. package/internal/board/card_test.go +41 -5
  119. package/internal/board/column.go +44 -13
  120. package/internal/board/column_test.go +5 -2
  121. package/internal/board/detail.go +0 -47
  122. package/internal/board/epic_panel.go +118 -22
  123. package/internal/board/epic_panel_test.go +302 -17
  124. package/internal/board/help.go +1 -0
  125. package/internal/board/help_test.go +1 -0
  126. package/internal/board/integration_test.go +266 -0
  127. package/internal/board/interfaces.go +65 -0
  128. package/internal/board/interfaces_test.go +114 -0
  129. package/internal/board/io.go +93 -0
  130. package/internal/board/model.go +79 -118
  131. package/internal/board/plain.go +88 -0
  132. package/internal/board/plain_test.go +117 -0
  133. package/internal/board/release.go +1 -9
  134. package/internal/board/release_test.go +6 -6
  135. package/internal/board/status.go +4 -4
  136. package/internal/board/theme.go +24 -0
  137. package/internal/board/theme_test.go +31 -0
  138. package/internal/board/transitions.go +113 -88
  139. package/internal/board/transitions_test.go +164 -141
  140. package/internal/board/tui.go +32 -0
  141. package/internal/board/update.go +325 -215
  142. package/internal/board/update_test.go +299 -18
  143. package/internal/board/util.go +76 -0
  144. package/internal/board/view.go +31 -28
  145. package/internal/board/view_test.go +12 -2
  146. package/internal/board/watch.go +35 -5
  147. package/internal/buildtool/main.go +2 -10
  148. package/internal/buildtool/main_test.go +46 -0
  149. package/internal/data/config.go +17 -3
  150. package/internal/data/config_test.go +49 -0
  151. package/internal/data/discover.go +26 -0
  152. package/internal/data/discover_test.go +34 -10
  153. package/internal/data/errors.go +4 -0
  154. package/internal/data/lifecycle.go +13 -6
  155. package/internal/data/lifecycle_test.go +14 -11
  156. package/internal/data/parser.go +21 -6
  157. package/internal/data/parser_test.go +31 -7
  158. package/internal/data/task.go +0 -9
  159. package/internal/data/write.go +85 -11
  160. package/internal/data/write_test.go +167 -0
  161. package/internal/doctor/checks.go +567 -0
  162. package/internal/doctor/checks_test.go +716 -0
  163. package/internal/doctor/gates.go +193 -0
  164. package/internal/doctor/gates_test.go +166 -0
  165. package/internal/doctor/interfaces.go +64 -0
  166. package/internal/doctor/interfaces_test.go +104 -0
  167. package/internal/doctor/repairs.go +80 -0
  168. package/internal/doctor/repairs_test.go +81 -0
  169. package/internal/doctor/report.go +157 -0
  170. package/internal/doctor/report_test.go +89 -0
  171. package/internal/init/clipboard.go +146 -0
  172. package/internal/init/clipboard_test.go +74 -0
  173. package/internal/init/install.go +16 -0
  174. package/internal/init/integration_test.go +197 -0
  175. package/internal/init/prompt.go +14 -0
  176. package/internal/init/prompt_test.go +77 -0
  177. package/internal/init/scaffold.go +59 -0
  178. package/internal/init/scaffold_test.go +179 -0
  179. package/internal/init/template_freshness_test.go +56 -0
  180. package/internal/init/validate.go +85 -0
  181. package/internal/init/validate_test.go +141 -0
  182. package/internal/init/write.go +73 -0
  183. package/internal/init/write_test.go +91 -0
  184. package/internal/styles/styles_test.go +133 -0
  185. package/internal/testutil/fixture.go +113 -0
  186. package/internal/testutil/fs.go +26 -0
  187. package/main.go +101 -4
  188. package/package.json +2 -2
  189. package/project-audit/audit_report_glm_5.1.md +411 -0
  190. package/project-audit/audit_report_opus_4.6 +406 -0
  191. package/project-audit/consolidated-audit-report.md +456 -0
  192. package/savepoint +0 -0
  193. package/templates/project/.savepoint/Design.md +2 -2
  194. package/templates/project/.savepoint/router.md +10 -10
  195. package/templates/project/AGENTS.md +33 -21
  196. package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
  197. package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
  198. package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
  199. package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
  200. package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
  201. package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
  202. package/templates/prompts/audit-reconciliation.prompt.md +33 -28
  203. package/templates/prompts/design.prompt.md +3 -1
  204. package/.savepoint/audit/v1/E01/proposals.md +0 -168
  205. package/.savepoint/audit/v1/E01/snapshot.md +0 -78
  206. package/.savepoint/audit/v1/E01-go-setup/proposals.md +0 -166
  207. package/.savepoint/audit/v1/E01-go-setup/snapshot.md +0 -71
  208. package/.savepoint/audit/v1/E01-scaffolding/proposals/AGENTS.md +0 -66
  209. package/.savepoint/audit/v1/E01-scaffolding/proposals/Design.md +0 -210
  210. package/.savepoint/audit/v1/E01-scaffolding/proposals/epic-Design.md +0 -117
  211. package/.savepoint/audit/v1/E01-scaffolding/proposals/quality-review.md +0 -101
  212. package/.savepoint/audit/v1/E01-scaffolding/snapshot.md +0 -54
  213. package/.savepoint/audit/v1/E02-data-model/snapshot.md +0 -128
  214. package/.savepoint/audit/v1/E02-data-readers/proposals.md +0 -123
  215. package/.savepoint/audit/v1/E02-data-readers/snapshot.md +0 -54
  216. package/.savepoint/audit/v1/E03-board-tui-core/proposals.md +0 -146
  217. package/.savepoint/audit/v1/E03-board-tui-core/snapshot.md +0 -57
  218. package/.savepoint/audit/v1/E03-cli-foundation/snapshot.md +0 -106
  219. package/.savepoint/audit/v1/E04-board-components/proposals.md +0 -118
  220. package/.savepoint/audit/v1/E04-board-components/snapshot.md +0 -77
  221. package/.savepoint/audit/v1/E04-templates-and-prompts/snapshot.md +0 -115
  222. package/.savepoint/audit/v1/E05-init-command/snapshot.md +0 -125
  223. package/.savepoint/audit/v1/E05-phase-transitions/proposals.md +0 -83
  224. package/.savepoint/audit/v1/E05-phase-transitions/snapshot.md +0 -36
  225. package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +0 -130
  226. package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +0 -84
  227. package/.savepoint/audit/v1/E06-tui-board/snapshot.md +0 -64
  228. package/.savepoint/audit/v1/E07-audit-pipeline/snapshot.md +0 -165
  229. package/.savepoint/audit/v1/E08-board-workflow-cleanup/snapshot.md +0 -65
  230. package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +0 -41
  231. package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +0 -48
  232. package/ink-cli-ui-design.zip +0 -0
  233. package/savepoint.exe +0 -0
@@ -0,0 +1,117 @@
1
+ package board
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/opencode/savepoint/internal/data"
10
+ )
11
+
12
+ func TestRenderPlainTable_warningBanner(t *testing.T) {
13
+ m := NewModel(nil, "", "")
14
+ got := RenderPlainTable(m)
15
+ if !strings.Contains(got, plainNonTTYWarning) {
16
+ t.Errorf("RenderPlainTable missing warning banner, got:\n%s", got)
17
+ }
18
+ }
19
+
20
+ func TestRenderPlainTable_columnHeaders(t *testing.T) {
21
+ m := NewModel(nil, "", "")
22
+ got := RenderPlainTable(m)
23
+ for _, header := range []string{"PLANNED", "IN PROGRESS", "DONE"} {
24
+ if !strings.Contains(got, header) {
25
+ t.Errorf("RenderPlainTable missing column header %q", header)
26
+ }
27
+ }
28
+ }
29
+
30
+ func TestRenderPlainTable_taskIDsAndTitles(t *testing.T) {
31
+ tasks := []data.Task{
32
+ {ID: "E08/T001", Title: "CLI entrypoint", Column: data.ColumnDone},
33
+ {ID: "E08/T002", Title: "Non-TTY fallback", Column: data.ColumnInProgress},
34
+ {ID: "E08/T003", Title: "TUI app shell", Column: data.ColumnPlanned},
35
+ }
36
+ m := NewModel(tasks, "", "")
37
+ got := RenderPlainTable(m)
38
+
39
+ for _, want := range []string{"E08/T001", "CLI entrypoint", "E08/T002", "Non-TTY fallback", "E08/T003", "TUI app shell"} {
40
+ if !strings.Contains(got, want) {
41
+ t.Errorf("RenderPlainTable missing %q", want)
42
+ }
43
+ }
44
+ }
45
+
46
+ func TestRenderPlainTable_noneWhenColumnEmpty(t *testing.T) {
47
+ m := NewModel(nil, "", "")
48
+ got := RenderPlainTable(m)
49
+ if !strings.Contains(got, "(none)") {
50
+ t.Errorf("RenderPlainTable should show (none) for empty columns")
51
+ }
52
+ }
53
+
54
+ func TestRenderPlainTable_auditSignalWhenProposalsExist(t *testing.T) {
55
+ root := t.TempDir()
56
+ epicDir := filepath.Join(root, "releases", "v1", "epics", "E01-test")
57
+ if err := os.MkdirAll(epicDir, 0755); err != nil {
58
+ t.Fatal(err)
59
+ }
60
+ auditContent := "---\ntype: audit\n---\n## Main Findings\nOK\n\n## Proposed Changes\n\n### Target File\nfoo.go\n"
61
+ if err := os.WriteFile(filepath.Join(epicDir, "E01-Audit.md"), []byte(auditContent), 0644); err != nil {
62
+ t.Fatal(err)
63
+ }
64
+
65
+ m := NewModel(nil, "", "")
66
+ m.Root = root
67
+ got := RenderPlainTable(m)
68
+ if !strings.Contains(got, plainAuditSignal) {
69
+ t.Errorf("RenderPlainTable missing audit signal when proposals exist, got:\n%s", got)
70
+ }
71
+ }
72
+
73
+ func TestRenderPlainTable_noAuditSignalWhenNone(t *testing.T) {
74
+ root := t.TempDir()
75
+ m := NewModel(nil, "", "")
76
+ m.Root = root
77
+ got := RenderPlainTable(m)
78
+ if strings.Contains(got, plainAuditSignal) {
79
+ t.Errorf("RenderPlainTable should not show audit signal when no proposals exist")
80
+ }
81
+ }
82
+
83
+ func TestHasAuditProposals_detectsSection(t *testing.T) {
84
+ root := t.TempDir()
85
+ epicDir := filepath.Join(root, "releases", "v1", "epics", "E02-slug")
86
+ if err := os.MkdirAll(epicDir, 0755); err != nil {
87
+ t.Fatal(err)
88
+ }
89
+ content := "## Main Findings\nAll good.\n\n## Proposed Changes\n\n### Target File\nbar.go\n"
90
+ if err := os.WriteFile(filepath.Join(epicDir, "E02-Audit.md"), []byte(content), 0644); err != nil {
91
+ t.Fatal(err)
92
+ }
93
+ if !hasAuditProposals(root) {
94
+ t.Error("hasAuditProposals should return true when Proposed Changes section exists")
95
+ }
96
+ }
97
+
98
+ func TestHasAuditProposals_noProposals(t *testing.T) {
99
+ root := t.TempDir()
100
+ epicDir := filepath.Join(root, "releases", "v1", "epics", "E03-slug")
101
+ if err := os.MkdirAll(epicDir, 0755); err != nil {
102
+ t.Fatal(err)
103
+ }
104
+ content := "## Main Findings\nAll good.\n"
105
+ if err := os.WriteFile(filepath.Join(epicDir, "E03-Audit.md"), []byte(content), 0644); err != nil {
106
+ t.Fatal(err)
107
+ }
108
+ if hasAuditProposals(root) {
109
+ t.Error("hasAuditProposals should return false when no Proposed Changes section")
110
+ }
111
+ }
112
+
113
+ func TestHasAuditProposals_missingRoot(t *testing.T) {
114
+ if hasAuditProposals("/nonexistent/path/xyz") {
115
+ t.Error("hasAuditProposals should return false for missing root")
116
+ }
117
+ }
@@ -31,12 +31,4 @@ func RenderReleaseDropdown(releases []string, cursor int, width int) string {
31
31
  return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
32
32
  }
33
33
 
34
- // releaseIndex returns the index of selected in releases, or 0 if not found.
35
- func releaseIndex(releases []string, selected string) int {
36
- for i, r := range releases {
37
- if r == selected {
38
- return i
39
- }
40
- }
41
- return 0
42
- }
34
+
@@ -40,17 +40,17 @@ func TestRenderReleaseDropdown_hintsPresent(t *testing.T) {
40
40
  }
41
41
  }
42
42
 
43
- func TestReleaseIndex_found(t *testing.T) {
43
+ func TestSliceIndexRelease_found(t *testing.T) {
44
44
  releases := []string{"v1", "v2", "v3"}
45
- if got := releaseIndex(releases, "v2"); got != 1 {
46
- t.Errorf("releaseIndex = %d, want 1", got)
45
+ if got := sliceIndex(releases, "v2"); got != 1 {
46
+ t.Errorf("sliceIndex = %d, want 1", got)
47
47
  }
48
48
  }
49
49
 
50
- func TestReleaseIndex_notFound(t *testing.T) {
50
+ func TestSliceIndexRelease_notFound(t *testing.T) {
51
51
  releases := []string{"v1", "v2"}
52
- if got := releaseIndex(releases, "v9"); got != 0 {
53
- t.Errorf("releaseIndex = %d, want 0", got)
52
+ if got := sliceIndex(releases, "v9"); got != 0 {
53
+ t.Errorf("sliceIndex = %d, want 0", got)
54
54
  }
55
55
  }
56
56
 
@@ -9,13 +9,13 @@ const statusGlyphDefault = " "
9
9
 
10
10
  func statusGlyph(status string) string {
11
11
  switch status {
12
- case string(data.StatusPlanned):
12
+ case string(data.ColumnPlanned):
13
13
  return styles.CardMeta.Render("○")
14
- case string(data.StatusInProgress):
14
+ case string(data.ColumnInProgress):
15
15
  return styles.GlyphBuild.Render("▶")
16
- case string(data.StatusDone):
16
+ case string(data.ColumnDone):
17
17
  return styles.TagDone.Render("◉")
18
- case string(data.StatusAudited):
18
+ case "audited":
19
19
  return styles.TagDone.Render("✓")
20
20
  default:
21
21
  return statusGlyphDefault
@@ -0,0 +1,24 @@
1
+ package board
2
+
3
+ import (
4
+ "os"
5
+
6
+ "github.com/charmbracelet/lipgloss"
7
+ "github.com/muesli/termenv"
8
+ )
9
+
10
+ // applyColorProfile detects terminal color support and configures lipgloss.
11
+ // Priority: NO_COLOR → COLORTERM env hint → termenv auto-detect.
12
+ func applyColorProfile() {
13
+ lipgloss.SetColorProfile(detectProfile())
14
+ }
15
+
16
+ func detectProfile() termenv.Profile {
17
+ if os.Getenv("NO_COLOR") != "" {
18
+ return termenv.Ascii
19
+ }
20
+ if c := os.Getenv("COLORTERM"); c == "truecolor" || c == "24bit" {
21
+ return termenv.TrueColor
22
+ }
23
+ return termenv.ColorProfile()
24
+ }
@@ -0,0 +1,31 @@
1
+ package board
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/muesli/termenv"
7
+ )
8
+
9
+ func TestDetectProfileNOCOLOR(t *testing.T) {
10
+ t.Setenv("NO_COLOR", "1")
11
+ t.Setenv("COLORTERM", "")
12
+ if got := detectProfile(); got != termenv.Ascii {
13
+ t.Fatalf("want Ascii, got %v", got)
14
+ }
15
+ }
16
+
17
+ func TestDetectProfileTruecolor(t *testing.T) {
18
+ t.Setenv("NO_COLOR", "")
19
+ t.Setenv("COLORTERM", "truecolor")
20
+ if got := detectProfile(); got != termenv.TrueColor {
21
+ t.Fatalf("want TrueColor, got %v", got)
22
+ }
23
+ }
24
+
25
+ func TestDetectProfile24bit(t *testing.T) {
26
+ t.Setenv("NO_COLOR", "")
27
+ t.Setenv("COLORTERM", "24bit")
28
+ if got := detectProfile(); got != termenv.TrueColor {
29
+ t.Fatalf("want TrueColor, got %v", got)
30
+ }
31
+ }
@@ -1,88 +1,113 @@
1
- package board
2
-
3
- import (
4
- "fmt"
5
-
6
- "github.com/opencode/savepoint/internal/data"
7
- )
8
-
9
- // Advance moves a task forward through the phase lifecycle.
10
- func Advance(t *data.Task) {
11
- switch t.Column {
12
- case data.ColumnPlanned:
13
- t.Column = data.ColumnInProgress
14
- t.Stage = data.StageBuild
15
- case data.ColumnInProgress:
16
- switch t.Stage {
17
- case data.StageBuild:
18
- t.Stage = data.StageTest
19
- case data.StageTest:
20
- t.Stage = data.StageAudit
21
- case data.StageAudit:
22
- t.Column = data.ColumnDone
23
- t.Stage = ""
24
- }
25
- }
26
- }
27
-
28
- // Retreat moves a task backward through the phase lifecycle.
29
- func Retreat(t *data.Task) {
30
- switch t.Column {
31
- case data.ColumnDone:
32
- t.Column = data.ColumnInProgress
33
- t.Stage = data.StageAudit
34
- case data.ColumnInProgress:
35
- switch t.Stage {
36
- case data.StageAudit:
37
- t.Stage = data.StageTest
38
- case data.StageTest:
39
- t.Stage = data.StageBuild
40
- case data.StageBuild:
41
- t.Column = data.ColumnPlanned
42
- t.Stage = ""
43
- }
44
- }
45
- }
46
-
47
- // CanAdvance checks whether a task is allowed to advance to its next phase.
48
- // It validates phase adjacency and dependency completion.
49
- // Returns (true, "") if allowed, or (false, reason) if blocked.
50
- func CanAdvance(t *data.Task, allTasks []data.Task) (bool, string) {
51
- switch t.Column {
52
- case data.ColumnPlanned:
53
- return true, ""
54
- case data.ColumnInProgress:
55
- switch t.Stage {
56
- case data.StageBuild:
57
- return true, ""
58
- case data.StageTest:
59
- return true, ""
60
- case data.StageAudit:
61
- for _, depID := range t.DependsOn {
62
- dep := findTask(depID, allTasks)
63
- if dep == nil {
64
- return false, fmt.Sprintf("dependency %q not found", depID)
65
- }
66
- if dep.Column != data.ColumnDone {
67
- return false, fmt.Sprintf("dependency %q is not done", depID)
68
- }
69
- }
70
- return true, ""
71
- default:
72
- return false, fmt.Sprintf("unknown stage %q", t.Stage)
73
- }
74
- case data.ColumnDone:
75
- return false, "task is already done"
76
- default:
77
- return false, fmt.Sprintf("unknown column %q", t.Column)
78
- }
79
- }
80
-
81
- func findTask(id string, tasks []data.Task) *data.Task {
82
- for i := range tasks {
83
- if tasks[i].ID == id {
84
- return &tasks[i]
85
- }
86
- }
87
- return nil
88
- }
1
+ package board
2
+
3
+ import (
4
+ "fmt"
5
+
6
+ "github.com/opencode/savepoint/internal/data"
7
+ )
8
+
9
+ // Advance moves a task forward through the phase lifecycle.
10
+ func Advance(t *data.Task) {
11
+ stage := t.Stage
12
+ if stage == "" {
13
+ stage = data.StageBuild
14
+ }
15
+ switch t.Column {
16
+ case data.ColumnPlanned:
17
+ t.Column = data.ColumnInProgress
18
+ t.Stage = data.StageBuild
19
+ case data.ColumnInProgress:
20
+ switch stage {
21
+ case data.StageBuild:
22
+ t.Stage = data.StageTest
23
+ case data.StageTest:
24
+ t.Stage = data.StageAudit
25
+ case data.StageAudit:
26
+ t.Column = data.ColumnDone
27
+ t.Stage = ""
28
+ }
29
+ }
30
+ t.Status = string(t.Column)
31
+ }
32
+
33
+ // Retreat moves a task backward through the phase lifecycle.
34
+ func Retreat(t *data.Task) {
35
+ stage := t.Stage
36
+ if stage == "" {
37
+ stage = data.StageBuild
38
+ }
39
+ switch t.Column {
40
+ case data.ColumnDone:
41
+ t.Column = data.ColumnInProgress
42
+ t.Stage = data.StageAudit
43
+ case data.ColumnInProgress:
44
+ switch stage {
45
+ case data.StageAudit:
46
+ t.Stage = data.StageTest
47
+ case data.StageTest:
48
+ t.Stage = data.StageBuild
49
+ case data.StageBuild:
50
+ t.Column = data.ColumnPlanned
51
+ t.Stage = ""
52
+ }
53
+ }
54
+ t.Status = string(t.Column)
55
+ }
56
+
57
+ func taskTransitionMessage(prefix string, task data.Task) string {
58
+ if task.Column == data.ColumnInProgress {
59
+ return fmt.Sprintf("%s %s to %s", prefix, shortID(task.ID), task.Stage)
60
+ }
61
+ return fmt.Sprintf("%s %s to %s", prefix, shortID(task.ID), task.Column)
62
+ }
63
+
64
+ // CanAdvance checks whether a task is allowed to advance to its next phase.
65
+ // It validates phase adjacency and dependency completion.
66
+ // Returns (true, "") if allowed, or (false, reason) if blocked.
67
+ func CanAdvance(t *data.Task, allTasks []data.Task) (bool, string) {
68
+ switch t.Column {
69
+ case data.ColumnPlanned:
70
+ return dependenciesDone(t, allTasks)
71
+ case data.ColumnInProgress:
72
+ stage := t.Stage
73
+ if stage == "" {
74
+ stage = data.StageBuild
75
+ }
76
+ switch stage {
77
+ case data.StageBuild:
78
+ return true, ""
79
+ case data.StageTest:
80
+ return true, ""
81
+ case data.StageAudit:
82
+ return dependenciesDone(t, allTasks)
83
+ default:
84
+ return false, fmt.Sprintf("unknown stage %q", stage)
85
+ }
86
+ case data.ColumnDone:
87
+ return false, "task is already done"
88
+ default:
89
+ return false, fmt.Sprintf("unknown column %q", t.Column)
90
+ }
91
+ }
92
+
93
+ func dependenciesDone(t *data.Task, allTasks []data.Task) (bool, string) {
94
+ for _, depID := range t.DependsOn {
95
+ dep := findTask(depID, allTasks)
96
+ if dep == nil {
97
+ return false, fmt.Sprintf("dependency %q not found", depID)
98
+ }
99
+ if dep.Column != data.ColumnDone {
100
+ return false, fmt.Sprintf("dependency %q is not done", depID)
101
+ }
102
+ }
103
+ return true, ""
104
+ }
105
+
106
+ func findTask(id string, tasks []data.Task) *data.Task {
107
+ for i := range tasks {
108
+ if tasks[i].ID == id {
109
+ return &tasks[i]
110
+ }
111
+ }
112
+ return nil
113
+ }