savepoint 1.0.1 → 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 (278) hide show
  1. package/.claude/settings.local.json +15 -1
  2. package/.golangci.yml +11 -0
  3. package/.savepoint/Design.md +52 -46
  4. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +1 -1
  5. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +1 -1
  6. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +1 -1
  7. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +1 -1
  8. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/{Design.md → E06-Detail.md} +5 -3
  9. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +1 -1
  10. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +1 -1
  11. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +1 -1
  12. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +2 -0
  13. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/{Design.md → E01-Detail.md} +9 -1
  14. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/{T007-next-activity-header.md → T001-next-activity-header.md} +13 -12
  15. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +9 -9
  16. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +2 -2
  17. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +13 -12
  18. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +14 -13
  19. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +25 -15
  20. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T007-column-focus-border-stability.md +57 -0
  21. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md +124 -0
  22. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/{Design.md → E02-Detail.md} +12 -3
  23. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +11 -8
  24. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +12 -7
  25. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +9 -5
  26. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +30 -9
  27. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
  28. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +45 -0
  29. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T001-border-resize-fix.md +40 -0
  30. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T002-next-activity-below-header.md +64 -0
  31. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T003-checkbox-rendering-fix.md +56 -0
  32. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T005-unify-status-glyphs.md +65 -0
  33. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +36 -0
  34. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Audit.md +167 -0
  35. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Detail.md +51 -0
  36. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T001-sidebar-focusable-navigation.md +65 -0
  37. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T002-epic-detail-overlay.md +73 -0
  38. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T003-epic-status-glyphs.md +73 -0
  39. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
  40. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +54 -0
  41. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +45 -0
  42. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +40 -0
  43. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +47 -0
  44. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +98 -0
  45. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +33 -0
  46. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +62 -0
  47. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
  48. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
  49. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
  50. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
  51. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
  52. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
  53. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
  54. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
  55. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
  56. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
  57. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
  58. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
  59. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
  60. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
  61. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
  62. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
  63. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
  64. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
  65. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
  66. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
  67. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
  68. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
  69. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
  70. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
  71. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
  72. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
  73. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
  74. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
  75. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
  76. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
  77. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
  78. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
  79. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
  80. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
  81. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
  82. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
  83. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
  84. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
  85. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
  86. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
  87. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
  88. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
  89. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
  90. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
  91. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
  92. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
  93. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
  94. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
  95. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
  96. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
  97. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
  98. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
  99. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
  100. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
  101. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
  102. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
  103. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
  104. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
  105. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
  106. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
  107. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
  108. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
  109. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
  110. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
  111. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
  112. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
  113. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
  114. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +43 -0
  115. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
  116. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +28 -0
  117. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
  118. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
  119. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
  120. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
  121. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +28 -0
  122. package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
  123. package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
  124. package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
  125. package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
  126. package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
  127. package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
  128. package/.savepoint/releases/v1.1/v1.1-PRD.md +139 -0
  129. package/.savepoint/router.md +29 -108
  130. package/AGENTS.md +69 -111
  131. package/Makefile +19 -3
  132. package/README.md +6 -6
  133. package/agent-skills/savepoint-audit/SKILL.md +87 -35
  134. package/agent-skills/savepoint-build-task/SKILL.md +9 -4
  135. package/agent-skills/savepoint-create-plan/SKILL.md +10 -5
  136. package/agent-skills/savepoint-create-task/SKILL.md +44 -31
  137. package/agent-skills/savepoint-draft-prd/SKILL.md +8 -3
  138. package/agent-skills/savepoint-system-design/SKILL.md +8 -3
  139. package/agent_skills_test.go +91 -0
  140. package/cmd/board.go +59 -0
  141. package/cmd/board_test.go +137 -0
  142. package/cmd/doctor.go +53 -0
  143. package/cmd/doctor_test.go +146 -0
  144. package/cmd/init.go +63 -0
  145. package/cmd/init_test.go +104 -0
  146. package/internal/board/board.go +69 -49
  147. package/internal/board/board_test.go +83 -67
  148. package/internal/board/card.go +71 -20
  149. package/internal/board/card_test.go +141 -12
  150. package/internal/board/column.go +77 -11
  151. package/internal/board/column_test.go +63 -13
  152. package/internal/board/detail.go +107 -72
  153. package/internal/board/detail_test.go +117 -26
  154. package/internal/board/epic_panel.go +211 -18
  155. package/internal/board/epic_panel_test.go +637 -14
  156. package/internal/board/help.go +1 -0
  157. package/internal/board/help_test.go +1 -0
  158. package/internal/board/integration_test.go +266 -0
  159. package/internal/board/interfaces.go +65 -0
  160. package/internal/board/interfaces_test.go +114 -0
  161. package/internal/board/io.go +93 -0
  162. package/internal/board/layout.go +12 -2
  163. package/internal/board/layout_test.go +17 -0
  164. package/internal/board/model.go +130 -52
  165. package/internal/board/plain.go +88 -0
  166. package/internal/board/plain_test.go +117 -0
  167. package/internal/board/release.go +1 -9
  168. package/internal/board/release_test.go +6 -6
  169. package/internal/board/render_policy_test.go +77 -0
  170. package/internal/board/status.go +23 -0
  171. package/internal/board/theme.go +24 -0
  172. package/internal/board/theme_test.go +31 -0
  173. package/internal/board/transitions.go +113 -88
  174. package/internal/board/transitions_test.go +164 -141
  175. package/internal/board/tui.go +32 -0
  176. package/internal/board/update.go +472 -94
  177. package/internal/board/update_test.go +447 -0
  178. package/internal/board/util.go +76 -0
  179. package/internal/board/view.go +139 -22
  180. package/internal/board/view_test.go +171 -3
  181. package/internal/board/watch.go +57 -9
  182. package/internal/buildtool/main.go +211 -0
  183. package/internal/buildtool/main_test.go +46 -0
  184. package/internal/data/config.go +17 -3
  185. package/internal/data/config_test.go +49 -0
  186. package/internal/data/discover.go +26 -0
  187. package/internal/data/discover_test.go +34 -10
  188. package/internal/data/errors.go +4 -0
  189. package/internal/data/lifecycle.go +13 -6
  190. package/internal/data/lifecycle_test.go +14 -11
  191. package/internal/data/parser.go +29 -6
  192. package/internal/data/parser_test.go +66 -7
  193. package/internal/data/task.go +1 -0
  194. package/internal/data/write.go +85 -11
  195. package/internal/data/write_test.go +167 -0
  196. package/internal/doctor/checks.go +567 -0
  197. package/internal/doctor/checks_test.go +716 -0
  198. package/internal/doctor/gates.go +193 -0
  199. package/internal/doctor/gates_test.go +166 -0
  200. package/internal/doctor/interfaces.go +64 -0
  201. package/internal/doctor/interfaces_test.go +104 -0
  202. package/internal/doctor/repairs.go +80 -0
  203. package/internal/doctor/repairs_test.go +81 -0
  204. package/internal/doctor/report.go +157 -0
  205. package/internal/doctor/report_test.go +89 -0
  206. package/internal/init/clipboard.go +146 -0
  207. package/internal/init/clipboard_test.go +74 -0
  208. package/internal/init/install.go +16 -0
  209. package/internal/init/integration_test.go +197 -0
  210. package/internal/init/prompt.go +14 -0
  211. package/internal/init/prompt_test.go +77 -0
  212. package/internal/init/scaffold.go +59 -0
  213. package/internal/init/scaffold_test.go +179 -0
  214. package/internal/init/template_freshness_test.go +56 -0
  215. package/internal/init/validate.go +85 -0
  216. package/internal/init/validate_test.go +141 -0
  217. package/internal/init/write.go +73 -0
  218. package/internal/init/write_test.go +91 -0
  219. package/internal/styles/palette.go +3 -3
  220. package/internal/styles/styles.go +39 -12
  221. package/internal/styles/styles_test.go +133 -0
  222. package/internal/testutil/fixture.go +113 -0
  223. package/internal/testutil/fs.go +26 -0
  224. package/main.go +107 -1
  225. package/package.json +2 -2
  226. package/project-audit/audit_report_glm_5.1.md +411 -0
  227. package/project-audit/audit_report_opus_4.6 +406 -0
  228. package/project-audit/consolidated-audit-report.md +456 -0
  229. package/savepoint +0 -0
  230. package/templates/project/.savepoint/Design.md +2 -2
  231. package/templates/project/.savepoint/router.md +15 -14
  232. package/templates/project/AGENTS.md +56 -98
  233. package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
  234. package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
  235. package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
  236. package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
  237. package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
  238. package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
  239. package/templates/prompts/audit-reconciliation.prompt.md +35 -30
  240. package/templates/prompts/design.prompt.md +3 -1
  241. package/templates/prompts/epic-design.prompt.md +3 -3
  242. package/templates/prompts/task-breakdown.prompt.md +1 -1
  243. package/templates/prompts/task-building.prompt.md +1 -1
  244. package/templates/prompts/task-planning.prompt.md +1 -1
  245. package/.savepoint/audit/E01-go-setup/proposals.md +0 -166
  246. package/.savepoint/audit/E01-go-setup/snapshot.md +0 -71
  247. package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +0 -66
  248. package/.savepoint/audit/E01-scaffolding/proposals/Design.md +0 -210
  249. package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +0 -117
  250. package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +0 -101
  251. package/.savepoint/audit/E01-scaffolding/snapshot.md +0 -54
  252. package/.savepoint/audit/E02-data-model/snapshot.md +0 -128
  253. package/.savepoint/audit/E02-data-readers/proposals.md +0 -123
  254. package/.savepoint/audit/E02-data-readers/snapshot.md +0 -54
  255. package/.savepoint/audit/E03-board-tui-core/proposals.md +0 -146
  256. package/.savepoint/audit/E03-board-tui-core/snapshot.md +0 -57
  257. package/.savepoint/audit/E03-cli-foundation/snapshot.md +0 -106
  258. package/.savepoint/audit/E04-board-components/proposals.md +0 -118
  259. package/.savepoint/audit/E04-board-components/snapshot.md +0 -77
  260. package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +0 -115
  261. package/.savepoint/audit/E05-init-command/snapshot.md +0 -125
  262. package/.savepoint/audit/E05-phase-transitions/proposals.md +0 -83
  263. package/.savepoint/audit/E05-phase-transitions/snapshot.md +0 -36
  264. package/.savepoint/audit/E06-atari-noir-layout/proposals.md +0 -130
  265. package/.savepoint/audit/E06-atari-noir-layout/snapshot.md +0 -84
  266. package/.savepoint/audit/E06-tui-board/snapshot.md +0 -64
  267. package/.savepoint/audit/E07-audit-pipeline/snapshot.md +0 -165
  268. package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +0 -65
  269. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -36
  270. package/ink-cli-ui-design.zip +0 -0
  271. package/main.exe +0 -0
  272. package/savepoint.exe +0 -0
  273. /package/.savepoint/releases/v1/epics/E01-go-setup/{Design.md → E01-Detail.md} +0 -0
  274. /package/.savepoint/releases/v1/epics/E02-data-readers/{Design.md → E02-Detail.md} +0 -0
  275. /package/.savepoint/releases/v1/epics/E03-board-tui-core/{Design.md → E03-Detail.md} +0 -0
  276. /package/.savepoint/releases/v1/epics/E04-board-components/{Design.md → E04-Detail.md} +0 -0
  277. /package/.savepoint/releases/v1/epics/E05-phase-transitions/{Design.md → E05-Detail.md} +0 -0
  278. /package/.savepoint/releases/v1/{PRD.md → v1-PRD.md} +0 -0
@@ -5,11 +5,12 @@ import (
5
5
  "testing"
6
6
 
7
7
  "github.com/opencode/savepoint/internal/data"
8
+ "github.com/opencode/savepoint/internal/styles"
8
9
  )
9
10
 
10
11
  func TestRenderCard_containsID(t *testing.T) {
11
12
  task := data.Task{ID: "E04/T002", Title: "Build card", Stage: data.StageBuild}
12
- got := RenderCard(task, 30, false, "")
13
+ got := RenderCard(task, 30, false, nil)
13
14
  if !strings.Contains(got, "T002") {
14
15
  t.Error("RenderCard missing short task ID")
15
16
  }
@@ -17,7 +18,7 @@ func TestRenderCard_containsID(t *testing.T) {
17
18
 
18
19
  func TestRenderCard_containsTitle(t *testing.T) {
19
20
  task := data.Task{ID: "T1", Title: "My title", Stage: data.StageBuild}
20
- got := RenderCard(task, 30, false, "")
21
+ got := RenderCard(task, 30, false, nil)
21
22
  if !strings.Contains(got, "My title") {
22
23
  t.Error("RenderCard missing task title")
23
24
  }
@@ -25,7 +26,7 @@ func TestRenderCard_containsTitle(t *testing.T) {
25
26
 
26
27
  func TestRenderCard_containsBuildGlyph(t *testing.T) {
27
28
  task := data.Task{ID: "T1", Stage: data.StageBuild}
28
- got := RenderCard(task, 30, false, "")
29
+ got := RenderCard(task, 30, false, nil)
29
30
  if !strings.Contains(got, glyphBuild) {
30
31
  t.Errorf("RenderCard missing build glyph %q", glyphBuild)
31
32
  }
@@ -33,7 +34,7 @@ func TestRenderCard_containsBuildGlyph(t *testing.T) {
33
34
 
34
35
  func TestRenderCard_containsTestGlyph(t *testing.T) {
35
36
  task := data.Task{ID: "T1", Stage: data.StageTest}
36
- got := RenderCard(task, 30, false, "")
37
+ got := RenderCard(task, 30, false, nil)
37
38
  if !strings.Contains(got, glyphTest) {
38
39
  t.Errorf("RenderCard missing test glyph %q", glyphTest)
39
40
  }
@@ -41,7 +42,7 @@ func TestRenderCard_containsTestGlyph(t *testing.T) {
41
42
 
42
43
  func TestRenderCard_containsAuditGlyph(t *testing.T) {
43
44
  task := data.Task{ID: "T1", Stage: data.StageAudit}
44
- got := RenderCard(task, 30, false, "")
45
+ got := RenderCard(task, 30, false, nil)
45
46
  if !strings.Contains(got, glyphAudit) {
46
47
  t.Errorf("RenderCard missing audit glyph %q", glyphAudit)
47
48
  }
@@ -49,7 +50,7 @@ func TestRenderCard_containsAuditGlyph(t *testing.T) {
49
50
 
50
51
  func TestRenderCard_focusedDoesNotPanic(t *testing.T) {
51
52
  task := data.Task{ID: "T1", Title: "hello", Stage: data.StageBuild}
52
- got := RenderCard(task, 30, true, "")
53
+ got := RenderCard(task, 30, true, nil)
53
54
  if got == "" {
54
55
  t.Error("RenderCard focused returned empty string")
55
56
  }
@@ -58,7 +59,7 @@ func TestRenderCard_focusedDoesNotPanic(t *testing.T) {
58
59
  func TestRenderCard_titleWraps(t *testing.T) {
59
60
  long := "This is a very long title that should be wrapped for sure"
60
61
  task := data.Task{ID: "T1", Title: long, Stage: data.StageBuild}
61
- got := RenderCard(task, 20, false, "")
62
+ got := RenderCard(task, 20, false, nil)
62
63
  // full title as one line does not fit; it must be broken up
63
64
  if strings.Contains(got, long) {
64
65
  t.Error("RenderCard should wrap long title, not render it as one line")
@@ -72,7 +73,7 @@ func TestRenderCard_titleWraps(t *testing.T) {
72
73
  func TestRenderCard_idTruncated(t *testing.T) {
73
74
  long := "E04-board-components/T999-very-long-id"
74
75
  task := data.Task{ID: long, Stage: data.StageBuild}
75
- got := RenderCard(task, 20, false, "")
76
+ got := RenderCard(task, 20, false, nil)
76
77
  if strings.Contains(got, long) {
77
78
  t.Error("RenderCard should truncate long ID")
78
79
  }
@@ -106,20 +107,148 @@ func TestTruncate_maxOne(t *testing.T) {
106
107
 
107
108
  func TestRenderCard_defaultStageUsesBuildGlyph(t *testing.T) {
108
109
  task := data.Task{ID: "T1", Stage: ""}
109
- got := RenderCard(task, 30, false, "")
110
+ got := RenderCard(task, 30, false, nil)
110
111
  if !strings.Contains(got, glyphBuild) {
111
112
  t.Error("RenderCard with empty stage should use build glyph")
112
113
  }
113
114
  }
114
115
 
115
116
  func TestRenderCard_routerPriorityUsesGreenGlyph(t *testing.T) {
116
- task := data.Task{ID: "E06/T009", Stage: data.StageTest}
117
- got := RenderCard(task, 30, false, "E06/T009")
117
+ task := data.Task{ID: "E06/T009", Release: "v1", Epic: "E06", Stage: data.StageTest}
118
+ router := &data.RouterState{Release: "v1", Epic: "E06", Task: "E06/T009"}
119
+ got := RenderCard(task, 30, false, router)
120
+ if !isRouterPriority(task, router) {
121
+ t.Error("router priority should match release, epic, and task")
122
+ }
118
123
  if !strings.Contains(got, glyphBuild) {
119
124
  t.Error("router priority card should use build glyph")
120
125
  }
121
- nonPriority := RenderCard(task, 30, false, "")
126
+ nonPriority := RenderCard(task, 30, false, nil)
122
127
  if !strings.Contains(nonPriority, glyphTest) {
123
128
  t.Error("non-priority test card should use test glyph")
124
129
  }
125
130
  }
131
+
132
+ func TestRenderCard_noBackgroundFillEscapes(t *testing.T) {
133
+ task := data.Task{ID: "E06/T009", Title: "Router priority", Release: "v1", Epic: "E06", Stage: data.StageTest}
134
+ router := &data.RouterState{Release: "v1", Epic: "E06", Task: "E06/T009"}
135
+ got := RenderCard(task, 30, false, router)
136
+ if strings.Contains(got, "\x1b[48;") || strings.Contains(got, "\x1b[40m") {
137
+ t.Fatalf("RenderCard should not emit background fills; got %q", got)
138
+ }
139
+ }
140
+
141
+ func TestRenderCard_routerPriorityMatchesShortID(t *testing.T) {
142
+ // Router stores short IDs ("T009"); task ID is full slug — must still match.
143
+ task := data.Task{ID: "E06-atari-noir-layout/T009-router-priority", Release: "v1", Epic: "E06-atari-noir-layout", Stage: data.StageTest}
144
+ router := &data.RouterState{Release: "v1", Epic: "E06", Task: "T009"}
145
+ got := RenderCard(task, 30, false, router)
146
+ if !isRouterPriority(task, router) {
147
+ t.Error("short router task ID should match full task ID slug")
148
+ }
149
+ if !strings.Contains(got, glyphBuild) {
150
+ t.Error("router priority card should use build glyph")
151
+ }
152
+ }
153
+
154
+ func TestRenderCard_staleRouterTaskNoMatch(t *testing.T) {
155
+ // Task moved to a new epic; router still has old epic path — should NOT match a different task number.
156
+ task := data.Task{ID: "E03-header-activity/T001-border-resize-fix", Release: "v1", Epic: "E03-header-activity", Stage: data.StageBuild}
157
+ router := &data.RouterState{Release: "v1", Epic: "E03", Task: "T002"}
158
+ got := RenderCard(task, 30, false, router)
159
+ if isRouterPriority(task, router) {
160
+ t.Error("stale router pointing to different task number should not show green glyph")
161
+ }
162
+ if !strings.Contains(got, styles.GlyphBuild.Render(glyphBuild)) {
163
+ t.Error("non-priority build task should use orange build glyph")
164
+ }
165
+ }
166
+
167
+ func TestRenderCard_routerSameTaskNumberDifferentEpicNoMatch(t *testing.T) {
168
+ task := data.Task{ID: "E03-header-activity/T001-border-resize-fix", Release: "v1", Epic: "E03-header-activity", Stage: data.StageTest}
169
+ router := &data.RouterState{Release: "v1", Epic: "E01", Task: "T001"}
170
+ got := RenderCard(task, 30, false, router)
171
+ if isRouterPriority(task, router) {
172
+ t.Error("router priority should not match same task number in a different epic")
173
+ }
174
+ if !strings.Contains(got, styles.GlyphTest.Render(glyphTest)) {
175
+ t.Error("non-priority test task should keep test glyph")
176
+ }
177
+ }
178
+
179
+ func TestRenderCard_doneTaskUsesOrangeBuildGlyph(t *testing.T) {
180
+ task := data.Task{ID: "E03/T001", Release: "v1", Epic: "E03", Column: data.ColumnDone, Stage: data.StageTest}
181
+ router := &data.RouterState{Release: "v1", Epic: "E03", Task: "T001"}
182
+ got := RenderCard(task, 30, false, router)
183
+ if !isRouterPriority(task, router) {
184
+ t.Error("router state should still identify the matching done task")
185
+ }
186
+ if !strings.Contains(got, styles.GlyphBuild.Render(glyphBuild)) {
187
+ t.Error("done task should use orange build glyph")
188
+ }
189
+ if strings.Contains(got, glyphTest) {
190
+ t.Error("done task should not use test glyph")
191
+ }
192
+ }
193
+
194
+ func TestRenderCard_explicitStatusUsesUnifiedGlyph(t *testing.T) {
195
+ tests := []struct {
196
+ name string
197
+ status string
198
+ glyph string
199
+ }{
200
+ {"planned", string(data.ColumnPlanned), "○"},
201
+ {"done", string(data.ColumnDone), "◉"},
202
+ {"audited", "audited", "✓"},
203
+ }
204
+
205
+ for _, tt := range tests {
206
+ t.Run(tt.name, func(t *testing.T) {
207
+ task := data.Task{ID: "T1", Status: string(tt.status), Stage: data.StageAudit}
208
+ got := RenderCard(task, 30, false, nil)
209
+ if !strings.Contains(got, tt.glyph) {
210
+ t.Errorf("RenderCard with status %q missing glyph %q", tt.status, tt.glyph)
211
+ }
212
+ if strings.Contains(got, glyphAudit) {
213
+ t.Errorf("RenderCard with status %q should not fall back to audit glyph", tt.status)
214
+ }
215
+ })
216
+ }
217
+ }
218
+
219
+ func TestRenderCard_inProgressShowsPhaseText(t *testing.T) {
220
+ tests := []struct {
221
+ name string
222
+ stage data.ProgressStage
223
+ label string
224
+ glyph string
225
+ }{
226
+ {"build", data.StageBuild, "BUILD", glyphBuild},
227
+ {"test", data.StageTest, "TEST", glyphTest},
228
+ {"audit", data.StageAudit, "AUDIT", glyphAudit},
229
+ }
230
+
231
+ for _, tt := range tests {
232
+ t.Run(tt.name, func(t *testing.T) {
233
+ task := data.Task{ID: "T1", Column: data.ColumnInProgress, Status: string(data.ColumnInProgress), Stage: tt.stage}
234
+ got := RenderCard(task, 30, false, nil)
235
+ if !strings.Contains(got, tt.label) {
236
+ t.Errorf("RenderCard missing phase label %q", tt.label)
237
+ }
238
+ if !strings.Contains(got, tt.glyph) {
239
+ t.Errorf("RenderCard missing phase glyph %q", tt.glyph)
240
+ }
241
+ if strings.Contains(got, "▶") {
242
+ t.Error("RenderCard should not use generic in_progress glyph when phase is available")
243
+ }
244
+ })
245
+ }
246
+ }
247
+
248
+ func TestRenderCard_doneShowsDoneText(t *testing.T) {
249
+ task := data.Task{ID: "T1", Column: data.ColumnDone, Status: string(data.ColumnDone)}
250
+ got := RenderCard(task, 30, false, nil)
251
+ if !strings.Contains(got, "DONE") {
252
+ t.Error("RenderCard missing DONE phase label")
253
+ }
254
+ }
@@ -8,12 +8,13 @@ import (
8
8
  "github.com/opencode/savepoint/internal/styles"
9
9
  )
10
10
 
11
- // RenderColumn renders a board column: header with label+count, task list, bordered container.
12
- func RenderColumn(tasks []data.Task, col data.ColumnType, width, focusedTask int, focused bool, routerTaskID string) string {
11
+ // RenderColumn renders a board column: header with label+count, task viewport, bordered container.
12
+ func RenderColumn(tasks []data.Task, col data.ColumnType, width, maxHeight, offset, focusedTask int, focused bool, routerState *data.RouterState) string {
13
13
  inner := width - colOverhead
14
14
  if inner < minColWidth {
15
15
  inner = minColWidth
16
16
  }
17
+ offset = clampViewportOffset(offset, len(tasks))
17
18
 
18
19
  title := columnTitle(col)
19
20
  header := fmt.Sprintf("%s (%d)", title, len(tasks))
@@ -27,19 +28,90 @@ func RenderColumn(tasks []data.Task, col data.ColumnType, width, focusedTask int
27
28
  if len(tasks) == 0 {
28
29
  lines = append(lines, styles.TaskItem.Render("(empty)"))
29
30
  } else {
30
- for i, t := range tasks {
31
- lines = append(lines, RenderCard(t, inner, focused && i == focusedTask, routerTaskID))
31
+ contentBudget := maxHeight - 2
32
+ if contentBudget < 1 {
33
+ contentBudget = 1
34
+ }
35
+
36
+ reserveAbove := 0
37
+ if offset > 0 {
38
+ reserveAbove = 1
39
+ }
40
+
41
+ type cardEntry struct {
42
+ card string
43
+ lines int
44
+ }
45
+ cardEntries := make([]cardEntry, 0, len(tasks)-offset)
46
+ for i := offset; i < len(tasks); i++ {
47
+ c := RenderCard(tasks[i], inner, focused && i == focusedTask, routerState)
48
+ cardEntries = append(cardEntries, cardEntry{card: c, lines: strings.Count(c, "\n") + 1})
49
+ }
50
+
51
+ usedLines := reserveAbove
52
+ endIdx := 0
53
+ for endIdx < len(cardEntries) {
54
+ needsMore := endIdx < len(cardEntries)-1
55
+ bottomReserve := 0
56
+ if needsMore {
57
+ bottomReserve = 1
58
+ }
59
+ if usedLines+cardEntries[endIdx].lines+bottomReserve > contentBudget {
60
+ break
61
+ }
62
+ usedLines += cardEntries[endIdx].lines
63
+ endIdx++
64
+ }
65
+
66
+ if endIdx == 0 && len(cardEntries) > 0 {
67
+ endIdx = 1
68
+ }
69
+
70
+ if offset > 0 {
71
+ lines = append(lines, renderScrollIndicator("↑", offset, "above"))
72
+ }
73
+ for i := 0; i < endIdx; i++ {
74
+ lines = append(lines, cardEntries[i].card)
75
+ }
76
+ if endIdx < len(cardEntries) {
77
+ remaining := len(tasks) - (offset + endIdx)
78
+ lines = append(lines, renderScrollIndicator("↓", remaining, "more"))
32
79
  }
33
80
  }
34
81
 
35
82
  content := strings.Join(lines, "\n")
36
- st := styles.Column.Width(width)
83
+ st := styles.ColumnUnfocused.Width(width)
37
84
  if focused {
38
85
  st = styles.ColumnFocused.Width(width)
39
86
  }
40
87
  return st.Render(content)
41
88
  }
42
89
 
90
+ func visibleColumnTaskLimit(maxHeight int) int {
91
+ if maxHeight <= 0 {
92
+ return 999999
93
+ }
94
+ limit := (maxHeight - 2) / 3
95
+ if limit < 1 {
96
+ return 1
97
+ }
98
+ return limit
99
+ }
100
+
101
+ func clampViewportOffset(offset, total int) int {
102
+ if offset < 0 || total <= 0 {
103
+ return 0
104
+ }
105
+ if offset >= total {
106
+ return total - 1
107
+ }
108
+ return offset
109
+ }
110
+
111
+ func renderScrollIndicator(arrow string, count int, suffix string) string {
112
+ return styles.ScrollIndicator.Render(fmt.Sprintf("%s %d %s", arrow, count, suffix))
113
+ }
114
+
43
115
  func columnTitle(col data.ColumnType) string {
44
116
  switch col {
45
117
  case data.ColumnPlanned:
@@ -53,9 +125,3 @@ func columnTitle(col data.ColumnType) string {
53
125
  }
54
126
  }
55
127
 
56
- func taskLabel(t data.Task) string {
57
- if t.Title == "" {
58
- return t.ID
59
- }
60
- return fmt.Sprintf("%s %s", t.ID, t.Title)
61
- }
@@ -4,11 +4,12 @@ import (
4
4
  "strings"
5
5
  "testing"
6
6
 
7
+ "github.com/charmbracelet/lipgloss"
7
8
  "github.com/opencode/savepoint/internal/data"
8
9
  )
9
10
 
10
11
  func TestRenderColumn_headerContainsLabel(t *testing.T) {
11
- got := RenderColumn(nil, data.ColumnPlanned, 30, 0, false, "")
12
+ got := RenderColumn(nil, data.ColumnPlanned, 30, 0, 0, 0, false, nil)
12
13
  if !strings.Contains(got, "PLANNED") {
13
14
  t.Error("RenderColumn missing PLANNED label")
14
15
  }
@@ -16,14 +17,14 @@ func TestRenderColumn_headerContainsLabel(t *testing.T) {
16
17
 
17
18
  func TestRenderColumn_headerContainsCount(t *testing.T) {
18
19
  tasks := []data.Task{{ID: "T1", Title: "Task one", Column: data.ColumnPlanned}}
19
- got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, false, "")
20
+ got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, 0, 0, false, nil)
20
21
  if !strings.Contains(got, "(1)") {
21
22
  t.Error("RenderColumn missing task count")
22
23
  }
23
24
  }
24
25
 
25
26
  func TestRenderColumn_emptyShowsPlaceholder(t *testing.T) {
26
- got := RenderColumn(nil, data.ColumnDone, 30, 0, false, "")
27
+ got := RenderColumn(nil, data.ColumnDone, 30, 0, 0, 0, false, nil)
27
28
  if !strings.Contains(got, "(empty)") {
28
29
  t.Error("RenderColumn missing (empty) for empty column")
29
30
  }
@@ -31,7 +32,7 @@ func TestRenderColumn_emptyShowsPlaceholder(t *testing.T) {
31
32
 
32
33
  func TestRenderColumn_focusedDoesNotPanic(t *testing.T) {
33
34
  tasks := []data.Task{{ID: "T1", Column: data.ColumnInProgress}}
34
- got := RenderColumn(tasks, data.ColumnInProgress, 30, 0, true, "")
35
+ got := RenderColumn(tasks, data.ColumnInProgress, 30, 0, 0, 0, true, nil)
35
36
  if got == "" {
36
37
  t.Error("RenderColumn returned empty string for focused column")
37
38
  }
@@ -47,7 +48,7 @@ func TestRenderColumn_allColumnTitles(t *testing.T) {
47
48
  {data.ColumnDone, "DONE"},
48
49
  }
49
50
  for _, tc := range cases {
50
- got := RenderColumn(nil, tc.col, 30, 0, false, "")
51
+ got := RenderColumn(nil, tc.col, 30, 0, 0, 0, false, nil)
51
52
  if !strings.Contains(got, tc.label) {
52
53
  t.Errorf("RenderColumn missing label %q for col %q", tc.label, tc.col)
53
54
  }
@@ -56,7 +57,7 @@ func TestRenderColumn_allColumnTitles(t *testing.T) {
56
57
 
57
58
  func TestRenderColumn_taskTitleRendered(t *testing.T) {
58
59
  tasks := []data.Task{{ID: "T2", Title: "Build it", Column: data.ColumnPlanned}}
59
- got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, false, "")
60
+ got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, 0, 0, false, nil)
60
61
  if !strings.Contains(got, "Build it") {
61
62
  t.Error("RenderColumn missing task title")
62
63
  }
@@ -64,26 +65,75 @@ func TestRenderColumn_taskTitleRendered(t *testing.T) {
64
65
 
65
66
  func TestRenderColumn_rendersTaskCards(t *testing.T) {
66
67
  tasks := []data.Task{{ID: "T2", Title: "Build it", Column: data.ColumnPlanned, Stage: data.StageAudit}}
67
- got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, true, "")
68
+ got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, 0, 0, true, nil)
68
69
  if !strings.Contains(got, glyphAudit) {
69
70
  t.Error("RenderColumn should render task phase glyph from card")
70
71
  }
71
- if !strings.Contains(got, "") {
72
+ if !strings.Contains(got, "") {
72
73
  t.Error("RenderColumn should render focused card border")
73
74
  }
74
75
  }
75
76
 
76
- func TestRenderColumn_unfocusedUsesSurfaceWithoutBorder(t *testing.T) {
77
+ func TestRenderColumn_focusStatesUseStableBorderDimensions(t *testing.T) {
77
78
  tasks := []data.Task{{ID: "T2", Title: "Build it", Column: data.ColumnPlanned, Stage: data.StageAudit}}
78
- got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, false, "")
79
- if strings.Contains(got, "╭") {
80
- t.Error("unfocused column/card should not render rounded borders")
79
+ unfocused := RenderColumn(tasks, data.ColumnPlanned, 30, 0, 0, -1, false, nil)
80
+ focused := RenderColumn(tasks, data.ColumnPlanned, 30, 0, 0, -1, true, nil)
81
+
82
+ if !strings.Contains(unfocused, "┌") {
83
+ t.Error("unfocused column should render a single-line border")
84
+ }
85
+ if !strings.Contains(focused, "┌") {
86
+ t.Error("focused column should render a single-line border")
87
+ }
88
+
89
+ unfocusedLines := strings.Split(unfocused, "\n")
90
+ focusedLines := strings.Split(focused, "\n")
91
+ if len(unfocusedLines) != len(focusedLines) {
92
+ t.Fatalf("line count changed between focus states: unfocused=%d focused=%d", len(unfocusedLines), len(focusedLines))
93
+ }
94
+ for i := range unfocusedLines {
95
+ if lipgloss.Width(unfocusedLines[i]) != lipgloss.Width(focusedLines[i]) {
96
+ t.Fatalf("line %d width changed between focus states: unfocused=%d focused=%d", i, lipgloss.Width(unfocusedLines[i]), lipgloss.Width(focusedLines[i]))
97
+ }
81
98
  }
82
99
  }
83
100
 
84
101
  func TestRenderColumn_emptyCountIsZero(t *testing.T) {
85
- got := RenderColumn(nil, data.ColumnPlanned, 30, 0, false, "")
102
+ got := RenderColumn(nil, data.ColumnPlanned, 30, 0, 0, 0, false, nil)
86
103
  if !strings.Contains(got, "(0)") {
87
104
  t.Error("RenderColumn missing (0) count for empty column")
88
105
  }
89
106
  }
107
+
108
+ func TestRenderColumn_viewportShowsScrollIndicators(t *testing.T) {
109
+ tasks := []data.Task{
110
+ {ID: "T1", Title: "Task one", Column: data.ColumnPlanned},
111
+ {ID: "T2", Title: "Task two", Column: data.ColumnPlanned},
112
+ {ID: "T3", Title: "Task three", Column: data.ColumnPlanned},
113
+ {ID: "T4", Title: "Task four", Column: data.ColumnPlanned},
114
+ }
115
+
116
+ got := RenderColumn(tasks, data.ColumnPlanned, 30, 8, 1, 1, true, nil)
117
+
118
+ if !strings.Contains(got, "↑ 1 above") {
119
+ t.Error("RenderColumn missing above indicator")
120
+ }
121
+ if !strings.Contains(got, "↓ 2 more") {
122
+ t.Errorf("RenderColumn missing more indicator, got:\n%s", got)
123
+ }
124
+ if strings.Contains(got, "Task one") {
125
+ t.Error("RenderColumn should not render tasks above viewport")
126
+ }
127
+ if strings.Contains(got, "Task three") {
128
+ t.Error("RenderColumn should not render tasks that don't fit budget")
129
+ }
130
+ if strings.Contains(got, "Task four") {
131
+ t.Error("RenderColumn should not render tasks below viewport")
132
+ }
133
+ }
134
+
135
+ func TestVisibleColumnTaskLimitDefaultsToFourAtStandardHeight(t *testing.T) {
136
+ if got := visibleColumnTaskLimit(CalculateLayout(100, 24).ContentHeight); got != 4 {
137
+ t.Errorf("visibleColumnTaskLimit(standard height) = %d, want 4", got)
138
+ }
139
+ }