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.
- package/.claude/settings.local.json +20 -0
- package/.prettierignore +4 -0
- package/.savepoint/Design.md +196 -0
- package/.savepoint/PRD.md +58 -0
- package/.savepoint/audit/E01-go-setup/proposals.md +166 -0
- package/.savepoint/audit/E01-go-setup/snapshot.md +71 -0
- package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +66 -0
- package/.savepoint/audit/E01-scaffolding/proposals/Design.md +210 -0
- package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +117 -0
- package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +101 -0
- package/.savepoint/audit/E01-scaffolding/snapshot.md +54 -0
- package/.savepoint/audit/E02-data-model/snapshot.md +128 -0
- package/.savepoint/audit/E02-data-readers/proposals.md +123 -0
- package/.savepoint/audit/E02-data-readers/snapshot.md +54 -0
- package/.savepoint/audit/E03-board-tui-core/proposals.md +146 -0
- package/.savepoint/audit/E03-board-tui-core/snapshot.md +57 -0
- package/.savepoint/audit/E03-cli-foundation/snapshot.md +106 -0
- package/.savepoint/audit/E04-board-components/proposals.md +118 -0
- package/.savepoint/audit/E04-board-components/snapshot.md +77 -0
- package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +115 -0
- package/.savepoint/audit/E05-init-command/snapshot.md +125 -0
- package/.savepoint/audit/E05-phase-transitions/proposals.md +83 -0
- package/.savepoint/audit/E05-phase-transitions/snapshot.md +36 -0
- package/.savepoint/audit/E06-tui-board/snapshot.md +64 -0
- package/.savepoint/audit/E07-audit-pipeline/snapshot.md +165 -0
- package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +65 -0
- package/.savepoint/config.yml +27 -0
- package/.savepoint/releases/v1/PRD.md +66 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/Design.md +39 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +42 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T002-entrypoint.md +23 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T003-directory-structure.md +24 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T004-makefile.md +23 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/Design.md +61 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T001-task-struct.md +29 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T002-frontmatter-parser.md +30 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T003-router-reader.md +29 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T004-config-reader.md +29 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T005-discovery.md +30 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/Design.md +38 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T001-model.md +29 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T002-update-loop.md +30 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T003-view.md +34 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T004-styles.md +29 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +42 -0
- package/.savepoint/releases/v1/epics/E04-board-components/Design.md +44 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T001-column.md +34 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +33 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T003-epic-panel.md +49 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T004-detail-overlay.md +40 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T005-release-dropdown.md +33 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +34 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/Design.md +38 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T001-phase-stepping.md +29 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T002-gates.md +31 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T003-write-task.md +31 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T004-write-router.md +31 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/Design.md +42 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T001-color-system.md +39 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +52 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +52 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +53 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T005-restore-nav-hints.md +39 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T007-detail-card-fixes.md +36 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T008-checkbox-states.md +38 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T009-router-priority-marker.md +41 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +61 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/Design.md +39 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T001-archive-epics.md +20 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T002-rewrite-prd.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T003-create-epic-stubs.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T004-update-router.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/Design.md +118 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/handoff.md +9 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T001-package-baseline.md +45 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T002-typescript-build.md +48 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T003-vitest-smoke.md +43 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T004-lint-format-gates.md +45 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T005-scaffold-verification.md +40 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/Design.md +142 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T001-domain-ids-status.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T002-markdown-frontmatter-boundary.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T003-task-documents.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T004-release-epic-router-config-readers.md +30 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T005-dependency-validation.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T006-epic-task-set-reader.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T007-quality-gates.md +31 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/Design.md +40 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T001-phase-types.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T002-phase-frontmatter.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T003-simplify-config.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T004-simplify-router-domain.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/Design.md +122 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T001-argument-parser-contract.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T002-help-text-generation.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T003-terminal-environment-detection.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T004-command-stub-modules.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T005-cli-runner-dispatch.md +34 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T006-entrypoint-quality-gates.md +32 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/Design.md +43 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T001-strip-args.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T002-strip-help.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T003-strip-run.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T004-delete-commands.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T005-update-cli-tests.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/Design.md +48 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T001-board-data-phases.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T002-phase-rendering.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T003-detail-pane-phases.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T004-phase-transitions.md +42 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T005-phase-gates.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T006-phase-write-back.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T007-remove-audit-flow.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T008-board-tests.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/Design.md +85 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T001-project-template-assets.md +17 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T002-release-and-prompt-assets.md +20 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T003-template-registry-renderer.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T004-template-integrity-tests.md +17 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T005-template-closeout-quality-gates.md +16 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/Design.md +88 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T001-init-cli-contract.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T002-target-validation.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T003-scaffold-writer.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T004-magic-prompt-and-clipboard.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T005-dev-deps-install-option.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T006-init-command-integration.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/Design.md +53 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T001-delete-dead-src.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T002-delete-dead-tests.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T003-delete-assets.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T004-clean-savepoint.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T005-rewrite-agents-md.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T006-clean-package-json.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T007-verify.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/Design.md +104 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T001-board-command-data.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T002-board-view-state.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T003-transition-gates-and-writes.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T004-terminal-theme.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T005-ink-board-ui.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T006-board-integration-audit-entry.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/Design.md +88 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T001-audit-cli-contract.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T002-quality-gate-runner.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T003-snapshot-and-prompt.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T004-audit-orchestration-router.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T005-proposal-validation-apply.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T006-audit-review-state.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T007-audit-review-ui.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T008-audit-pipeline-integration.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/Design.md +103 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T001-acceptance-criteria-model.md +30 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T002-release-task-set-reader.md +33 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T003-board-data-and-plain-output.md +34 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T004-board-selection-state.md +33 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T005-ink-board-layout-cleanup.md +37 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T006-task-detail-popup.md +36 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T007-templates-acceptance-criteria.md +34 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T008-board-workflow-integration.md +41 -0
- package/.savepoint/releases/v1/epics/_archived/E09-doctor-command/Design.md +70 -0
- package/.savepoint/releases/v1/epics/_archived/E10-docs-and-packaging/Design.md +68 -0
- package/.savepoint/releases/v1/epics/_archived/E11-release-validation/Design.md +68 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/Design.md +26 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +35 -0
- package/.savepoint/router.md +136 -0
- package/.savepoint/visual-identity.md +124 -0
- package/AGENTS.md +141 -0
- package/CLAUDE.md +1 -0
- package/GEMINI.md +1 -0
- package/LICENSE +21 -0
- package/Makefile +13 -0
- package/README.md +78 -0
- package/agent-skills/ink-tui-design/SKILL.md +309 -0
- package/agent-skills/ink-tui-design/references/component-patterns.md +371 -0
- package/agent-skills/ink-tui-design/references/hooks-guide.md +436 -0
- package/agent-skills/ink-tui-design/references/ink-gotchas.md +330 -0
- package/agent-skills/ink-tui-design/references/testing-patterns.md +384 -0
- package/agent-skills/savepoint-audit/SKILL.md +35 -0
- package/agent-skills/savepoint-build-task/SKILL.md +39 -0
- package/agent-skills/savepoint-create-plan/SKILL.md +28 -0
- package/agent-skills/savepoint-create-task/SKILL.md +31 -0
- package/agent-skills/savepoint-draft-prd/SKILL.md +32 -0
- package/agent-skills/savepoint-system-design/SKILL.md +33 -0
- package/agent-skills/superpowers/brainstorming/SKILL.md +165 -0
- package/agent-skills/superpowers/brainstorming/visual-companion.md +304 -0
- package/agent-skills/superpowers/dispatching-parallel-agents/SKILL.md +193 -0
- package/agent-skills/superpowers/executing-plans/SKILL.md +77 -0
- package/agent-skills/superpowers/finishing-a-development-branch/SKILL.md +213 -0
- package/agent-skills/superpowers/receiving-code-review/SKILL.md +226 -0
- package/agent-skills/superpowers/requesting-code-review/SKILL.md +115 -0
- package/agent-skills/superpowers/requesting-code-review/code-reviewer.md +160 -0
- package/agent-skills/superpowers/subagent-driven-development/SKILL.md +292 -0
- package/agent-skills/superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +27 -0
- package/agent-skills/superpowers/subagent-driven-development/implementer-prompt.md +113 -0
- package/agent-skills/superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/agent-skills/superpowers/systematic-debugging/SKILL.md +305 -0
- package/agent-skills/superpowers/systematic-debugging/condition-based-waiting.md +122 -0
- package/agent-skills/superpowers/systematic-debugging/defense-in-depth.md +130 -0
- package/agent-skills/superpowers/systematic-debugging/root-cause-tracing.md +183 -0
- package/agent-skills/superpowers/test-driven-development/SKILL.md +389 -0
- package/agent-skills/superpowers/test-driven-development/testing-anti-patterns.md +317 -0
- package/agent-skills/superpowers/verification-before-completion/SKILL.md +147 -0
- package/agent-skills/superpowers/writing-plans/SKILL.md +159 -0
- package/agent-skills/superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/assets/banner.png +0 -0
- package/assets/logo.png +0 -0
- package/assets/strawman.png +0 -0
- package/go.mod +33 -0
- package/go.sum +73 -0
- package/ink-cli-ui-design.zip +0 -0
- package/internal/board/board.go +121 -0
- package/internal/board/board_test.go +99 -0
- package/internal/board/card.go +72 -0
- package/internal/board/card_test.go +111 -0
- package/internal/board/column.go +61 -0
- package/internal/board/column_test.go +81 -0
- package/internal/board/detail.go +140 -0
- package/internal/board/detail_test.go +233 -0
- package/internal/board/epic_panel.go +69 -0
- package/internal/board/epic_panel_test.go +246 -0
- package/internal/board/help.go +40 -0
- package/internal/board/help_test.go +85 -0
- package/internal/board/layout.go +58 -0
- package/internal/board/layout_test.go +89 -0
- package/internal/board/model.go +151 -0
- package/internal/board/model_test.go +67 -0
- package/internal/board/release.go +42 -0
- package/internal/board/release_test.go +177 -0
- package/internal/board/transitions.go +88 -0
- package/internal/board/transitions_test.go +141 -0
- package/internal/board/update.go +155 -0
- package/internal/board/update_test.go +128 -0
- package/internal/board/view.go +190 -0
- package/internal/board/view_test.go +147 -0
- package/internal/data/config.go +87 -0
- package/internal/data/config_test.go +73 -0
- package/internal/data/discover.go +152 -0
- package/internal/data/discover_test.go +106 -0
- package/internal/data/errors.go +9 -0
- package/internal/data/lifecycle.go +37 -0
- package/internal/data/lifecycle_test.go +38 -0
- package/internal/data/parser.go +189 -0
- package/internal/data/parser_test.go +216 -0
- package/internal/data/router.go +52 -0
- package/internal/data/router_test.go +35 -0
- package/internal/data/task.go +46 -0
- package/internal/data/task_test.go +51 -0
- package/internal/data/write.go +144 -0
- package/internal/data/write_test.go +456 -0
- package/internal/styles/palette.go +47 -0
- package/internal/styles/styles.go +122 -0
- package/main.exe +0 -0
- package/main.go +11 -0
- package/package.json +25 -0
- package/savepoint +0 -0
- package/savepoint.exe +0 -0
- package/scripts/vitest-preload.cjs +95 -0
- package/templates/project/.savepoint/Design.md +47 -0
- package/templates/project/.savepoint/PRD.md +34 -0
- package/templates/project/.savepoint/config.yml +27 -0
- package/templates/project/.savepoint/router.md +152 -0
- package/templates/project/.savepoint/visual-identity.md +122 -0
- package/templates/project/AGENTS.md +130 -0
- package/templates/prompts/audit-reconciliation.prompt.md +67 -0
- package/templates/prompts/design.prompt.md +43 -0
- package/templates/prompts/epic-design.prompt.md +43 -0
- package/templates/prompts/magic-prompt.prompt.md +7 -0
- package/templates/prompts/prd.prompt.md +42 -0
- package/templates/prompts/task-breakdown.prompt.md +54 -0
- package/templates/prompts/task-building.prompt.md +38 -0
- package/templates/prompts/task-planning.prompt.md +53 -0
- package/templates/release/v1/PRD.md +37 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
"github.com/opencode/savepoint/internal/data"
|
|
7
|
+
"github.com/opencode/savepoint/internal/styles"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
const detailBorderPad = 4 // rounded border (2) + padding (2×1)
|
|
11
|
+
|
|
12
|
+
// RenderDetail renders a task detail overlay panel at the given display width.
|
|
13
|
+
func RenderDetail(t data.Task, overlayW int) string {
|
|
14
|
+
inner := overlayW - detailBorderPad
|
|
15
|
+
if inner < 4 {
|
|
16
|
+
inner = 4
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
lines := []string{
|
|
20
|
+
styles.ColumnTitleFocused.Render("TASK DETAIL"),
|
|
21
|
+
strings.Repeat("─", inner),
|
|
22
|
+
}
|
|
23
|
+
lines = append(lines,
|
|
24
|
+
detailRow("ID", t.ID, inner),
|
|
25
|
+
detailRow("Title", t.Title, inner),
|
|
26
|
+
detailRow("Epic", t.Epic, inner),
|
|
27
|
+
detailRow("Release", t.Release, inner),
|
|
28
|
+
detailRow("Status", string(t.Column), inner),
|
|
29
|
+
detailRow("Phase", phaseLabel(t.Stage), inner),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if t.Description != "" {
|
|
33
|
+
lines = append(lines,
|
|
34
|
+
"",
|
|
35
|
+
styles.ColumnTitle.Render("Description:"),
|
|
36
|
+
)
|
|
37
|
+
for _, line := range wrapText(t.Description, inner) {
|
|
38
|
+
lines = append(lines, styles.CardMeta.Render(line))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if len(t.Acceptance) > 0 {
|
|
43
|
+
lines = append(lines, "", styles.ColumnTitle.Render("Acceptance Criteria:"))
|
|
44
|
+
for _, a := range t.Acceptance {
|
|
45
|
+
for _, line := range wrapText(a, inner-2) {
|
|
46
|
+
lines = append(lines, styles.CardMeta.Render(" • "+line))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if len(t.Checklist) > 0 {
|
|
52
|
+
lines = append(lines, "", styles.ColumnTitle.Render("Implementation Plan:"))
|
|
53
|
+
for _, item := range t.Checklist {
|
|
54
|
+
for _, line := range wrapText(item, inner-2) {
|
|
55
|
+
lines = append(lines, styles.CardMeta.Render(" □ "+line))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
lines = append(lines, "", styles.CardMeta.Render("esc:close"))
|
|
61
|
+
|
|
62
|
+
return styles.DetailOverlay.Width(overlayW).Render(strings.Join(lines, "\n"))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func detailRow(label, value string, width int) string {
|
|
66
|
+
prefix := label + ": "
|
|
67
|
+
wrapped := wrapText(value, width-len(prefix))
|
|
68
|
+
if len(wrapped) == 0 {
|
|
69
|
+
wrapped = []string{""}
|
|
70
|
+
}
|
|
71
|
+
lines := make([]string, 0, len(wrapped))
|
|
72
|
+
for i, line := range wrapped {
|
|
73
|
+
if i == 0 {
|
|
74
|
+
lines = append(lines, styles.ColumnTitle.Render(prefix)+styles.CardMeta.Render(line))
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
lines = append(lines, strings.Repeat(" ", len(prefix))+styles.CardMeta.Render(line))
|
|
78
|
+
}
|
|
79
|
+
return strings.Join(lines, "\n")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func phaseLabel(s data.ProgressStage) string {
|
|
83
|
+
switch s {
|
|
84
|
+
case data.StageTest:
|
|
85
|
+
return "test"
|
|
86
|
+
case data.StageAudit:
|
|
87
|
+
return "audit"
|
|
88
|
+
default:
|
|
89
|
+
return "build"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func wrapText(s string, width int) []string {
|
|
94
|
+
if width < 4 {
|
|
95
|
+
width = 4
|
|
96
|
+
}
|
|
97
|
+
words := strings.Fields(s)
|
|
98
|
+
if len(words) == 0 {
|
|
99
|
+
return nil
|
|
100
|
+
}
|
|
101
|
+
lines := []string{}
|
|
102
|
+
current := ""
|
|
103
|
+
for _, word := range words {
|
|
104
|
+
if len([]rune(word)) > width {
|
|
105
|
+
if current != "" {
|
|
106
|
+
lines = append(lines, current)
|
|
107
|
+
current = ""
|
|
108
|
+
}
|
|
109
|
+
lines = append(lines, splitLongWord(word, width)...)
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
if current == "" {
|
|
113
|
+
current = word
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
if len([]rune(current))+1+len([]rune(word)) <= width {
|
|
117
|
+
current += " " + word
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
lines = append(lines, current)
|
|
121
|
+
current = word
|
|
122
|
+
}
|
|
123
|
+
if current != "" {
|
|
124
|
+
lines = append(lines, current)
|
|
125
|
+
}
|
|
126
|
+
return lines
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func splitLongWord(word string, width int) []string {
|
|
130
|
+
runes := []rune(word)
|
|
131
|
+
lines := []string{}
|
|
132
|
+
for len(runes) > width {
|
|
133
|
+
lines = append(lines, string(runes[:width]))
|
|
134
|
+
runes = runes[width:]
|
|
135
|
+
}
|
|
136
|
+
if len(runes) > 0 {
|
|
137
|
+
lines = append(lines, string(runes))
|
|
138
|
+
}
|
|
139
|
+
return lines
|
|
140
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
8
|
+
"github.com/opencode/savepoint/internal/data"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func sampleTask() data.Task {
|
|
12
|
+
return data.Task{
|
|
13
|
+
ID: "E04/T001",
|
|
14
|
+
Title: "My Task",
|
|
15
|
+
Epic: "E04-board-components",
|
|
16
|
+
Release: "v1",
|
|
17
|
+
Column: data.ColumnInProgress,
|
|
18
|
+
Stage: data.StageBuild,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func TestRenderDetail_containsID(t *testing.T) {
|
|
23
|
+
got := RenderDetail(sampleTask(), 60)
|
|
24
|
+
if !strings.Contains(got, "E04/T001") {
|
|
25
|
+
t.Error("RenderDetail missing task ID")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestRenderDetail_containsTitle(t *testing.T) {
|
|
30
|
+
got := RenderDetail(sampleTask(), 60)
|
|
31
|
+
if !strings.Contains(got, "My Task") {
|
|
32
|
+
t.Error("RenderDetail missing task title")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func TestRenderDetail_containsEpic(t *testing.T) {
|
|
37
|
+
got := RenderDetail(sampleTask(), 60)
|
|
38
|
+
if !strings.Contains(got, "E04-board-components") {
|
|
39
|
+
t.Error("RenderDetail missing epic")
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func TestRenderDetail_containsRelease(t *testing.T) {
|
|
44
|
+
got := RenderDetail(sampleTask(), 60)
|
|
45
|
+
if !strings.Contains(got, "v1") {
|
|
46
|
+
t.Error("RenderDetail missing release")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func TestRenderDetail_containsStatus(t *testing.T) {
|
|
51
|
+
got := RenderDetail(sampleTask(), 60)
|
|
52
|
+
if !strings.Contains(got, "in_progress") {
|
|
53
|
+
t.Error("RenderDetail missing status")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func TestRenderDetail_containsPhase(t *testing.T) {
|
|
58
|
+
got := RenderDetail(sampleTask(), 60)
|
|
59
|
+
if !strings.Contains(got, "build") {
|
|
60
|
+
t.Error("RenderDetail missing phase")
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func TestRenderDetail_containsEscHint(t *testing.T) {
|
|
65
|
+
got := RenderDetail(sampleTask(), 60)
|
|
66
|
+
if !strings.Contains(got, "esc") {
|
|
67
|
+
t.Error("RenderDetail missing esc:close hint")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func TestRenderDetail_containsDescription(t *testing.T) {
|
|
72
|
+
tk := sampleTask()
|
|
73
|
+
tk.Description = "some description text"
|
|
74
|
+
got := RenderDetail(tk, 60)
|
|
75
|
+
if !strings.Contains(got, "some description text") {
|
|
76
|
+
t.Error("RenderDetail missing description text")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func TestRenderDetail_noDescriptionSectionWhenEmpty(t *testing.T) {
|
|
81
|
+
got := RenderDetail(sampleTask(), 60)
|
|
82
|
+
if strings.Contains(got, "Description:") {
|
|
83
|
+
t.Error("RenderDetail should not show Description section when empty")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func TestRenderDetail_containsAcceptanceCriteria(t *testing.T) {
|
|
88
|
+
tk := sampleTask()
|
|
89
|
+
tk.Acceptance = []string{"criterion one", "criterion two"}
|
|
90
|
+
got := RenderDetail(tk, 60)
|
|
91
|
+
if !strings.Contains(got, "criterion one") {
|
|
92
|
+
t.Error("RenderDetail missing first acceptance criterion")
|
|
93
|
+
}
|
|
94
|
+
if !strings.Contains(got, "criterion two") {
|
|
95
|
+
t.Error("RenderDetail missing second acceptance criterion")
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func TestRenderDetail_containsChecklist(t *testing.T) {
|
|
100
|
+
tk := sampleTask()
|
|
101
|
+
tk.Checklist = []string{"first implementation item", "second implementation item"}
|
|
102
|
+
got := RenderDetail(tk, 60)
|
|
103
|
+
if !strings.Contains(got, "Implementation Plan:") {
|
|
104
|
+
t.Error("RenderDetail missing implementation plan heading")
|
|
105
|
+
}
|
|
106
|
+
if !strings.Contains(got, "first implementation item") {
|
|
107
|
+
t.Error("RenderDetail missing first checklist item")
|
|
108
|
+
}
|
|
109
|
+
if !strings.Contains(got, "second implementation item") {
|
|
110
|
+
t.Error("RenderDetail missing second checklist item")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func TestRenderDetail_wrapsLongDescription(t *testing.T) {
|
|
115
|
+
tk := sampleTask()
|
|
116
|
+
tk.Description = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda"
|
|
117
|
+
got := RenderDetail(tk, 30)
|
|
118
|
+
if strings.Contains(got, tk.Description) {
|
|
119
|
+
t.Error("RenderDetail should wrap long description text")
|
|
120
|
+
}
|
|
121
|
+
if !strings.Contains(got, "alpha beta") || !strings.Contains(got, "lambda") {
|
|
122
|
+
t.Error("RenderDetail should preserve wrapped description words")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func TestRenderDetail_noAcceptanceSectionWhenEmpty(t *testing.T) {
|
|
127
|
+
got := RenderDetail(sampleTask(), 60)
|
|
128
|
+
if strings.Contains(got, "Acceptance Criteria:") {
|
|
129
|
+
t.Error("RenderDetail should not show Acceptance section when empty")
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func TestPhaseLabel_build(t *testing.T) {
|
|
134
|
+
if got := phaseLabel(data.StageBuild); got != "build" {
|
|
135
|
+
t.Errorf("phaseLabel(StageBuild) = %q, want %q", got, "build")
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func TestPhaseLabel_test(t *testing.T) {
|
|
140
|
+
if got := phaseLabel(data.StageTest); got != "test" {
|
|
141
|
+
t.Errorf("phaseLabel(StageTest) = %q, want %q", got, "test")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func TestPhaseLabel_audit(t *testing.T) {
|
|
146
|
+
if got := phaseLabel(data.StageAudit); got != "audit" {
|
|
147
|
+
t.Errorf("phaseLabel(StageAudit) = %q, want %q", got, "audit")
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func TestPhaseLabel_default(t *testing.T) {
|
|
152
|
+
if got := phaseLabel(""); got != "build" {
|
|
153
|
+
t.Errorf("phaseLabel(%q) = %q, want %q", "", got, "build")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func TestUpdate_enterOpensDetailOverlay(t *testing.T) {
|
|
158
|
+
tasks := []data.Task{sampleTask()}
|
|
159
|
+
m := NewModel(tasks, "v1", "E04-board-components")
|
|
160
|
+
m.FocusedColumn = data.ColumnInProgress
|
|
161
|
+
m.FocusedTask = 0
|
|
162
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
|
163
|
+
updated := requireModel(t, got)
|
|
164
|
+
if updated.Overlay != OverlayDetail {
|
|
165
|
+
t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayDetail)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func TestUpdate_enterNoOpWhenNoTasks(t *testing.T) {
|
|
170
|
+
m := NewModel(nil, "v1", "E04-board-components")
|
|
171
|
+
m.FocusedColumn = data.ColumnPlanned
|
|
172
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
|
173
|
+
updated := requireModel(t, got)
|
|
174
|
+
if updated.Overlay != OverlayNone {
|
|
175
|
+
t.Errorf("Overlay = %q, want none when column has no tasks", updated.Overlay)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func TestUpdate_detailOverlayEscCloses(t *testing.T) {
|
|
180
|
+
m := NewModel(nil, "v1", "E04-board-components")
|
|
181
|
+
m.Overlay = OverlayDetail
|
|
182
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
|
|
183
|
+
updated := requireModel(t, got)
|
|
184
|
+
if updated.Overlay != OverlayNone {
|
|
185
|
+
t.Errorf("Overlay = %q after esc, want none", updated.Overlay)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func TestUpdate_detailOverlayBlocksColumnNav(t *testing.T) {
|
|
190
|
+
m := NewModel(nil, "v1", "E04-board-components")
|
|
191
|
+
m.Overlay = OverlayDetail
|
|
192
|
+
m.FocusedColumn = data.ColumnPlanned
|
|
193
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")})
|
|
194
|
+
updated := requireModel(t, got)
|
|
195
|
+
if updated.FocusedColumn != data.ColumnPlanned {
|
|
196
|
+
t.Error("column nav should be blocked when detail overlay is open")
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func TestView_detailOverlayRendered(t *testing.T) {
|
|
201
|
+
tasks := []data.Task{sampleTask()}
|
|
202
|
+
m := NewModel(tasks, "v1", "E04-board-components")
|
|
203
|
+
m.Width = 100
|
|
204
|
+
m.Height = 30
|
|
205
|
+
m.FocusedColumn = data.ColumnInProgress
|
|
206
|
+
m.FocusedTask = 0
|
|
207
|
+
m.Overlay = OverlayDetail
|
|
208
|
+
got := m.View()
|
|
209
|
+
if !strings.Contains(got, "TASK DETAIL") {
|
|
210
|
+
t.Error("View() with OverlayDetail missing TASK DETAIL header")
|
|
211
|
+
}
|
|
212
|
+
if !strings.Contains(got, "E04/T001") {
|
|
213
|
+
t.Error("View() with OverlayDetail missing task ID")
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func TestOverlayWidth_clampMax(t *testing.T) {
|
|
218
|
+
if got := overlayWidth(120); got != 80 {
|
|
219
|
+
t.Errorf("overlayWidth(120) = %d, want 80", got)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func TestOverlayWidth_termMinus4(t *testing.T) {
|
|
224
|
+
if got := overlayWidth(60); got != 56 {
|
|
225
|
+
t.Errorf("overlayWidth(60) = %d, want 56", got)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func TestOverlayWidth_clampMin(t *testing.T) {
|
|
230
|
+
if got := overlayWidth(10); got != 20 {
|
|
231
|
+
t.Errorf("overlayWidth(10) = %d, want 20", got)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
"github.com/opencode/savepoint/internal/styles"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
const epicActiveMarker = "►"
|
|
10
|
+
|
|
11
|
+
// RenderEpicSidebar renders the fixed left sidebar listing epics with active indicator.
|
|
12
|
+
// If epics is empty and selected is non-empty, selected is shown as the sole entry.
|
|
13
|
+
func RenderEpicSidebar(epics []string, selected string, width int) string {
|
|
14
|
+
inner := width - epicPanelOverhead
|
|
15
|
+
if inner < 2 {
|
|
16
|
+
inner = 2
|
|
17
|
+
}
|
|
18
|
+
list := epics
|
|
19
|
+
if len(list) == 0 && selected != "" {
|
|
20
|
+
list = []string{selected}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
lines := []string{styles.ColumnTitle.Render("EPICS"), strings.Repeat("─", inner)}
|
|
24
|
+
for _, e := range list {
|
|
25
|
+
label := truncate(e, inner-2)
|
|
26
|
+
if e == selected {
|
|
27
|
+
lines = append(lines, styles.TaskItemFocused.Render(epicActiveMarker+" "+label))
|
|
28
|
+
} else {
|
|
29
|
+
lines = append(lines, styles.TaskItem.Render(" "+label))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if len(list) == 0 {
|
|
33
|
+
lines = append(lines, styles.TaskItem.Render("(none)"))
|
|
34
|
+
}
|
|
35
|
+
return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// RenderEpicDropdown renders the epic selection dropdown overlay.
|
|
39
|
+
func RenderEpicDropdown(epics []string, cursor int, width int) string {
|
|
40
|
+
inner := width - epicPanelOverhead
|
|
41
|
+
if inner < 2 {
|
|
42
|
+
inner = 2
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
lines := []string{styles.ColumnTitleFocused.Render("SELECT EPIC"), strings.Repeat("─", inner)}
|
|
46
|
+
for i, e := range epics {
|
|
47
|
+
label := truncate(e, inner-2)
|
|
48
|
+
if i == cursor {
|
|
49
|
+
lines = append(lines, styles.TaskItemFocused.Render(epicActiveMarker+" "+label))
|
|
50
|
+
} else {
|
|
51
|
+
lines = append(lines, styles.TaskItem.Render(" "+label))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if len(epics) == 0 {
|
|
55
|
+
lines = append(lines, styles.TaskItem.Render("(none)"))
|
|
56
|
+
}
|
|
57
|
+
lines = append(lines, "", styles.CardMeta.Render("↑↓:nav enter:select esc:cancel"))
|
|
58
|
+
return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// epicIndex returns the index of selected in epics, or 0 if not found.
|
|
62
|
+
func epicIndex(epics []string, selected string) int {
|
|
63
|
+
for i, e := range epics {
|
|
64
|
+
if e == selected {
|
|
65
|
+
return i
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
8
|
+
"github.com/opencode/savepoint/internal/data"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func TestRenderEpicSidebar_containsEpicsHeader(t *testing.T) {
|
|
12
|
+
got := RenderEpicSidebar([]string{"E01", "E02"}, "E01", 28)
|
|
13
|
+
if !strings.Contains(got, "EPICS") {
|
|
14
|
+
t.Error("RenderEpicSidebar missing EPICS header")
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func TestRenderEpicSidebar_activeEpicMarked(t *testing.T) {
|
|
19
|
+
got := RenderEpicSidebar([]string{"E01", "E02"}, "E01", 28)
|
|
20
|
+
if !strings.Contains(got, epicActiveMarker) {
|
|
21
|
+
t.Errorf("RenderEpicSidebar missing active marker %q", epicActiveMarker)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestRenderEpicSidebar_allEpicsPresent(t *testing.T) {
|
|
26
|
+
epics := []string{"E01-foo", "E02-bar", "E03-baz"}
|
|
27
|
+
got := RenderEpicSidebar(epics, "E01-foo", 32)
|
|
28
|
+
for _, e := range epics {
|
|
29
|
+
if !strings.Contains(got, e) {
|
|
30
|
+
t.Errorf("RenderEpicSidebar missing epic %q", e)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func TestRenderEpicSidebar_emptyEpicsFallback(t *testing.T) {
|
|
36
|
+
got := RenderEpicSidebar(nil, "E03", 28)
|
|
37
|
+
if !strings.Contains(got, "E03") {
|
|
38
|
+
t.Error("RenderEpicSidebar with empty list should show selected epic")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func TestRenderEpicSidebar_emptyBothShowsNone(t *testing.T) {
|
|
43
|
+
got := RenderEpicSidebar(nil, "", 28)
|
|
44
|
+
if !strings.Contains(got, "(none)") {
|
|
45
|
+
t.Error("RenderEpicSidebar with no epics and no selected should show (none)")
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func TestRenderEpicDropdown_containsHeader(t *testing.T) {
|
|
50
|
+
got := RenderEpicDropdown([]string{"E01", "E02"}, 0, 32)
|
|
51
|
+
if !strings.Contains(got, "SELECT EPIC") {
|
|
52
|
+
t.Error("RenderEpicDropdown missing SELECT EPIC header")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func TestRenderEpicDropdown_cursorMarked(t *testing.T) {
|
|
57
|
+
got := RenderEpicDropdown([]string{"E01", "E02"}, 1, 32)
|
|
58
|
+
if !strings.Contains(got, epicActiveMarker) {
|
|
59
|
+
t.Errorf("RenderEpicDropdown missing cursor marker %q", epicActiveMarker)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func TestRenderEpicDropdown_containsHint(t *testing.T) {
|
|
64
|
+
got := RenderEpicDropdown([]string{"E01"}, 0, 32)
|
|
65
|
+
if !strings.Contains(got, "esc") {
|
|
66
|
+
t.Error("RenderEpicDropdown missing esc hint")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func TestRenderEpicDropdown_emptyShowsNone(t *testing.T) {
|
|
71
|
+
got := RenderEpicDropdown(nil, 0, 32)
|
|
72
|
+
if !strings.Contains(got, "(none)") {
|
|
73
|
+
t.Error("RenderEpicDropdown with no epics should show (none)")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func TestEpicIndex_found(t *testing.T) {
|
|
78
|
+
epics := []string{"E01", "E02", "E03"}
|
|
79
|
+
if got := epicIndex(epics, "E02"); got != 1 {
|
|
80
|
+
t.Errorf("epicIndex = %d, want 1", got)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func TestEpicIndex_notFound(t *testing.T) {
|
|
85
|
+
if got := epicIndex([]string{"E01"}, "E99"); got != 0 {
|
|
86
|
+
t.Errorf("epicIndex not-found = %d, want 0", got)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func TestEpicIndex_empty(t *testing.T) {
|
|
91
|
+
if got := epicIndex(nil, "E01"); got != 0 {
|
|
92
|
+
t.Errorf("epicIndex empty = %d, want 0", got)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Update integration tests for epic dropdown
|
|
97
|
+
|
|
98
|
+
func TestUpdate_eKeyOpensDropdownNarrow(t *testing.T) {
|
|
99
|
+
m := NewModel(nil, "v1", "E03")
|
|
100
|
+
m.Width = 80 // narrow: < 120
|
|
101
|
+
m.Epics = []string{"E01", "E03"}
|
|
102
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("e")})
|
|
103
|
+
updated := requireModel(t, got)
|
|
104
|
+
if updated.Overlay != OverlayEpic {
|
|
105
|
+
t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayEpic)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func TestUpdate_eKeyOpensDropdownWide(t *testing.T) {
|
|
110
|
+
m := NewModel(nil, "v1", "E03")
|
|
111
|
+
m.Width = 120
|
|
112
|
+
m.Epics = []string{"E01", "E03"}
|
|
113
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("e")})
|
|
114
|
+
updated := requireModel(t, got)
|
|
115
|
+
if updated.Overlay != OverlayEpic {
|
|
116
|
+
t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayEpic)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func TestUpdate_epicDropdownEscCloses(t *testing.T) {
|
|
121
|
+
m := NewModel(nil, "v1", "E03")
|
|
122
|
+
m.Overlay = OverlayEpic
|
|
123
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
|
|
124
|
+
updated := requireModel(t, got)
|
|
125
|
+
if updated.Overlay != OverlayNone {
|
|
126
|
+
t.Errorf("Overlay = %q after esc, want none", updated.Overlay)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func TestUpdate_epicDropdownDownMovesCursor(t *testing.T) {
|
|
131
|
+
m := NewModel(nil, "v1", "E01")
|
|
132
|
+
m.Overlay = OverlayEpic
|
|
133
|
+
m.Epics = []string{"E01", "E02", "E03"}
|
|
134
|
+
m.EpicCursor = 0
|
|
135
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyDown})
|
|
136
|
+
updated := requireModel(t, got)
|
|
137
|
+
if updated.EpicCursor != 1 {
|
|
138
|
+
t.Errorf("EpicCursor = %d, want 1", updated.EpicCursor)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func TestUpdate_epicDropdownUpMovesCursor(t *testing.T) {
|
|
143
|
+
m := NewModel(nil, "v1", "E02")
|
|
144
|
+
m.Overlay = OverlayEpic
|
|
145
|
+
m.Epics = []string{"E01", "E02", "E03"}
|
|
146
|
+
m.EpicCursor = 2
|
|
147
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyUp})
|
|
148
|
+
updated := requireModel(t, got)
|
|
149
|
+
if updated.EpicCursor != 1 {
|
|
150
|
+
t.Errorf("EpicCursor = %d, want 1", updated.EpicCursor)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func TestUpdate_epicDropdownEnterSelectsEpic(t *testing.T) {
|
|
155
|
+
tasks := []data.Task{
|
|
156
|
+
{ID: "T1", Epic: "E01", Release: "v1", Column: data.ColumnPlanned},
|
|
157
|
+
{ID: "T3", Epic: "E03", Release: "v1", Column: data.ColumnPlanned},
|
|
158
|
+
}
|
|
159
|
+
m := NewModel(tasks, "v1", "E01")
|
|
160
|
+
m.Overlay = OverlayEpic
|
|
161
|
+
m.Epics = []string{"E01", "E02", "E03"}
|
|
162
|
+
m.EpicCursor = 2
|
|
163
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
|
164
|
+
updated := requireModel(t, got)
|
|
165
|
+
if updated.SelectedEpic != "E03" {
|
|
166
|
+
t.Errorf("SelectedEpic = %q, want %q", updated.SelectedEpic, "E03")
|
|
167
|
+
}
|
|
168
|
+
if updated.Overlay != OverlayNone {
|
|
169
|
+
t.Errorf("Overlay = %q after enter, want none", updated.Overlay)
|
|
170
|
+
}
|
|
171
|
+
if got := len(updated.Tasks[data.ColumnPlanned]); got != 1 {
|
|
172
|
+
t.Errorf("planned task count = %d, want 1 after epic selection", got)
|
|
173
|
+
}
|
|
174
|
+
if updated.Tasks[data.ColumnPlanned][0].ID != "T3" {
|
|
175
|
+
t.Errorf("visible task = %q, want T3", updated.Tasks[data.ColumnPlanned][0].ID)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func TestUpdate_epicDropdownDownClampedAtEnd(t *testing.T) {
|
|
180
|
+
m := NewModel(nil, "v1", "E03")
|
|
181
|
+
m.Overlay = OverlayEpic
|
|
182
|
+
m.Epics = []string{"E01", "E02"}
|
|
183
|
+
m.EpicCursor = 1
|
|
184
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyDown})
|
|
185
|
+
updated := requireModel(t, got)
|
|
186
|
+
if updated.EpicCursor != 1 {
|
|
187
|
+
t.Errorf("EpicCursor = %d, want 1 (clamped)", updated.EpicCursor)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
func TestUpdate_epicDropdownUpClampedAtStart(t *testing.T) {
|
|
192
|
+
m := NewModel(nil, "v1", "E01")
|
|
193
|
+
m.Overlay = OverlayEpic
|
|
194
|
+
m.Epics = []string{"E01", "E02"}
|
|
195
|
+
m.EpicCursor = 0
|
|
196
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyUp})
|
|
197
|
+
updated := requireModel(t, got)
|
|
198
|
+
if updated.EpicCursor != 0 {
|
|
199
|
+
t.Errorf("EpicCursor = %d, want 0 (clamped)", updated.EpicCursor)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func TestUpdate_overlayBlocksColumnNav(t *testing.T) {
|
|
204
|
+
m := NewModel(nil, "v1", "E01")
|
|
205
|
+
m.Overlay = OverlayEpic
|
|
206
|
+
m.FocusedColumn = "planned"
|
|
207
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")})
|
|
208
|
+
updated := requireModel(t, got)
|
|
209
|
+
if updated.FocusedColumn != "planned" {
|
|
210
|
+
t.Error("column nav should be blocked when overlay is open")
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func TestView_epicDropdownOverlayRendered(t *testing.T) {
|
|
215
|
+
m := NewModel(nil, "v1", "E01")
|
|
216
|
+
m.Width = 80
|
|
217
|
+
m.Height = 24
|
|
218
|
+
m.Overlay = OverlayEpic
|
|
219
|
+
m.Epics = []string{"E01", "E02"}
|
|
220
|
+
got := m.View()
|
|
221
|
+
if !strings.Contains(got, "SELECT EPIC") {
|
|
222
|
+
t.Error("View() with OverlayEpic missing SELECT EPIC")
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func TestView_epicDropdownKeepsBoardBehind(t *testing.T) {
|
|
227
|
+
m := NewModel(nil, "v1", "E01")
|
|
228
|
+
m.Width = 100
|
|
229
|
+
m.Height = 24
|
|
230
|
+
m.Overlay = OverlayEpic
|
|
231
|
+
m.Epics = []string{"E01", "E02"}
|
|
232
|
+
got := m.View()
|
|
233
|
+
if !strings.Contains(got, "S A V E P O I N T") {
|
|
234
|
+
t.Error("View() with OverlayEpic should keep board visible behind overlay")
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func TestView_epicSidebarOnWide(t *testing.T) {
|
|
239
|
+
m := NewModel(nil, "v1", "E03")
|
|
240
|
+
m.Width = 120
|
|
241
|
+
m.Epics = []string{"E01", "E03"}
|
|
242
|
+
got := m.View()
|
|
243
|
+
if !strings.Contains(got, "EPICS") {
|
|
244
|
+
t.Error("View() at width>=120 missing EPICS header in sidebar")
|
|
245
|
+
}
|
|
246
|
+
}
|