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.
- package/.claude/settings.local.json +15 -1
- package/.golangci.yml +11 -0
- package/.savepoint/Design.md +52 -46
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +1 -1
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +1 -1
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +1 -1
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/{Design.md → E06-Detail.md} +5 -3
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +2 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/{Design.md → E01-Detail.md} +9 -1
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/{T007-next-activity-header.md → T001-next-activity-header.md} +13 -12
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +9 -9
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +2 -2
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +13 -12
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +14 -13
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +25 -15
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T007-column-focus-border-stability.md +57 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md +124 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/{Design.md → E02-Detail.md} +12 -3
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +11 -8
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +12 -7
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +9 -5
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +30 -9
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +45 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T001-border-resize-fix.md +40 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T002-next-activity-below-header.md +64 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T003-checkbox-rendering-fix.md +56 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T005-unify-status-glyphs.md +65 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +36 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Audit.md +167 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Detail.md +51 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T001-sidebar-focusable-navigation.md +65 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T002-epic-detail-overlay.md +73 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T003-epic-status-glyphs.md +73 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +54 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +45 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +40 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +47 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +98 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +33 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +62 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +43 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +28 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +28 -0
- package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
- package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
- package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
- package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
- package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
- package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
- package/.savepoint/releases/v1.1/v1.1-PRD.md +139 -0
- package/.savepoint/router.md +29 -108
- package/AGENTS.md +69 -111
- package/Makefile +19 -3
- package/README.md +6 -6
- package/agent-skills/savepoint-audit/SKILL.md +87 -35
- package/agent-skills/savepoint-build-task/SKILL.md +9 -4
- package/agent-skills/savepoint-create-plan/SKILL.md +10 -5
- package/agent-skills/savepoint-create-task/SKILL.md +44 -31
- package/agent-skills/savepoint-draft-prd/SKILL.md +8 -3
- package/agent-skills/savepoint-system-design/SKILL.md +8 -3
- package/agent_skills_test.go +91 -0
- package/cmd/board.go +59 -0
- package/cmd/board_test.go +137 -0
- package/cmd/doctor.go +53 -0
- package/cmd/doctor_test.go +146 -0
- package/cmd/init.go +63 -0
- package/cmd/init_test.go +104 -0
- package/internal/board/board.go +69 -49
- package/internal/board/board_test.go +83 -67
- package/internal/board/card.go +71 -20
- package/internal/board/card_test.go +141 -12
- package/internal/board/column.go +77 -11
- package/internal/board/column_test.go +63 -13
- package/internal/board/detail.go +107 -72
- package/internal/board/detail_test.go +117 -26
- package/internal/board/epic_panel.go +211 -18
- package/internal/board/epic_panel_test.go +637 -14
- package/internal/board/help.go +1 -0
- package/internal/board/help_test.go +1 -0
- package/internal/board/integration_test.go +266 -0
- package/internal/board/interfaces.go +65 -0
- package/internal/board/interfaces_test.go +114 -0
- package/internal/board/io.go +93 -0
- package/internal/board/layout.go +12 -2
- package/internal/board/layout_test.go +17 -0
- package/internal/board/model.go +130 -52
- package/internal/board/plain.go +88 -0
- package/internal/board/plain_test.go +117 -0
- package/internal/board/release.go +1 -9
- package/internal/board/release_test.go +6 -6
- package/internal/board/render_policy_test.go +77 -0
- package/internal/board/status.go +23 -0
- package/internal/board/theme.go +24 -0
- package/internal/board/theme_test.go +31 -0
- package/internal/board/transitions.go +113 -88
- package/internal/board/transitions_test.go +164 -141
- package/internal/board/tui.go +32 -0
- package/internal/board/update.go +472 -94
- package/internal/board/update_test.go +447 -0
- package/internal/board/util.go +76 -0
- package/internal/board/view.go +139 -22
- package/internal/board/view_test.go +171 -3
- package/internal/board/watch.go +57 -9
- package/internal/buildtool/main.go +211 -0
- package/internal/buildtool/main_test.go +46 -0
- package/internal/data/config.go +17 -3
- package/internal/data/config_test.go +49 -0
- package/internal/data/discover.go +26 -0
- package/internal/data/discover_test.go +34 -10
- package/internal/data/errors.go +4 -0
- package/internal/data/lifecycle.go +13 -6
- package/internal/data/lifecycle_test.go +14 -11
- package/internal/data/parser.go +29 -6
- package/internal/data/parser_test.go +66 -7
- package/internal/data/task.go +1 -0
- package/internal/data/write.go +85 -11
- package/internal/data/write_test.go +167 -0
- package/internal/doctor/checks.go +567 -0
- package/internal/doctor/checks_test.go +716 -0
- package/internal/doctor/gates.go +193 -0
- package/internal/doctor/gates_test.go +166 -0
- package/internal/doctor/interfaces.go +64 -0
- package/internal/doctor/interfaces_test.go +104 -0
- package/internal/doctor/repairs.go +80 -0
- package/internal/doctor/repairs_test.go +81 -0
- package/internal/doctor/report.go +157 -0
- package/internal/doctor/report_test.go +89 -0
- package/internal/init/clipboard.go +146 -0
- package/internal/init/clipboard_test.go +74 -0
- package/internal/init/install.go +16 -0
- package/internal/init/integration_test.go +197 -0
- package/internal/init/prompt.go +14 -0
- package/internal/init/prompt_test.go +77 -0
- package/internal/init/scaffold.go +59 -0
- package/internal/init/scaffold_test.go +179 -0
- package/internal/init/template_freshness_test.go +56 -0
- package/internal/init/validate.go +85 -0
- package/internal/init/validate_test.go +141 -0
- package/internal/init/write.go +73 -0
- package/internal/init/write_test.go +91 -0
- package/internal/styles/palette.go +3 -3
- package/internal/styles/styles.go +39 -12
- package/internal/styles/styles_test.go +133 -0
- package/internal/testutil/fixture.go +113 -0
- package/internal/testutil/fs.go +26 -0
- package/main.go +107 -1
- package/package.json +2 -2
- package/project-audit/audit_report_glm_5.1.md +411 -0
- package/project-audit/audit_report_opus_4.6 +406 -0
- package/project-audit/consolidated-audit-report.md +456 -0
- package/savepoint +0 -0
- package/templates/project/.savepoint/Design.md +2 -2
- package/templates/project/.savepoint/router.md +15 -14
- package/templates/project/AGENTS.md +56 -98
- package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
- package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
- package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
- package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
- package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
- package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
- package/templates/prompts/audit-reconciliation.prompt.md +35 -30
- package/templates/prompts/design.prompt.md +3 -1
- package/templates/prompts/epic-design.prompt.md +3 -3
- package/templates/prompts/task-breakdown.prompt.md +1 -1
- package/templates/prompts/task-building.prompt.md +1 -1
- package/templates/prompts/task-planning.prompt.md +1 -1
- package/.savepoint/audit/E01-go-setup/proposals.md +0 -166
- package/.savepoint/audit/E01-go-setup/snapshot.md +0 -71
- package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +0 -66
- package/.savepoint/audit/E01-scaffolding/proposals/Design.md +0 -210
- package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +0 -117
- package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +0 -101
- package/.savepoint/audit/E01-scaffolding/snapshot.md +0 -54
- package/.savepoint/audit/E02-data-model/snapshot.md +0 -128
- package/.savepoint/audit/E02-data-readers/proposals.md +0 -123
- package/.savepoint/audit/E02-data-readers/snapshot.md +0 -54
- package/.savepoint/audit/E03-board-tui-core/proposals.md +0 -146
- package/.savepoint/audit/E03-board-tui-core/snapshot.md +0 -57
- package/.savepoint/audit/E03-cli-foundation/snapshot.md +0 -106
- package/.savepoint/audit/E04-board-components/proposals.md +0 -118
- package/.savepoint/audit/E04-board-components/snapshot.md +0 -77
- package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +0 -115
- package/.savepoint/audit/E05-init-command/snapshot.md +0 -125
- package/.savepoint/audit/E05-phase-transitions/proposals.md +0 -83
- package/.savepoint/audit/E05-phase-transitions/snapshot.md +0 -36
- package/.savepoint/audit/E06-atari-noir-layout/proposals.md +0 -130
- package/.savepoint/audit/E06-atari-noir-layout/snapshot.md +0 -84
- package/.savepoint/audit/E06-tui-board/snapshot.md +0 -64
- package/.savepoint/audit/E07-audit-pipeline/snapshot.md +0 -165
- package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +0 -65
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -36
- package/ink-cli-ui-design.zip +0 -0
- package/main.exe +0 -0
- package/savepoint.exe +0 -0
- /package/.savepoint/releases/v1/epics/E01-go-setup/{Design.md → E01-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E02-data-readers/{Design.md → E02-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E03-board-tui-core/{Design.md → E03-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E04-board-components/{Design.md → E04-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E05-phase-transitions/{Design.md → E05-Detail.md} +0 -0
- /package/.savepoint/releases/v1/{PRD.md → v1-PRD.md} +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"archive/tar"
|
|
5
|
+
"compress/gzip"
|
|
6
|
+
"errors"
|
|
7
|
+
"flag"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"os"
|
|
11
|
+
"os/exec"
|
|
12
|
+
"strings"
|
|
13
|
+
"path/filepath"
|
|
14
|
+
"runtime"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
type target struct {
|
|
18
|
+
os string
|
|
19
|
+
arch string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var targets = []target{
|
|
23
|
+
{os: "linux", arch: "amd64"},
|
|
24
|
+
{os: "linux", arch: "arm64"},
|
|
25
|
+
{os: "darwin", arch: "amd64"},
|
|
26
|
+
{os: "darwin", arch: "arm64"},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var versionOverride string
|
|
30
|
+
|
|
31
|
+
func main() {
|
|
32
|
+
if err := run(os.Args[1:]); err != nil {
|
|
33
|
+
fmt.Fprintln(os.Stderr, err)
|
|
34
|
+
os.Exit(1)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func run(args []string) error {
|
|
39
|
+
flags := flag.NewFlagSet("buildtool", flag.ContinueOnError)
|
|
40
|
+
flags.StringVar(&versionOverride, "version", "", "version to inject into the binary")
|
|
41
|
+
if err := flags.Parse(args); err != nil {
|
|
42
|
+
return err
|
|
43
|
+
}
|
|
44
|
+
if flags.NArg() != 1 {
|
|
45
|
+
return errors.New("usage: go run ./internal/buildtool [-version vX.Y.Z] <build|clean|build-linux|build-darwin|build-all|dist|smoke-test>")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
switch flags.Arg(0) {
|
|
49
|
+
case "build":
|
|
50
|
+
return buildLocal()
|
|
51
|
+
case "clean":
|
|
52
|
+
return clean()
|
|
53
|
+
case "build-linux":
|
|
54
|
+
return buildMatching("linux")
|
|
55
|
+
case "build-darwin":
|
|
56
|
+
return buildMatching("darwin")
|
|
57
|
+
case "build-all":
|
|
58
|
+
return buildAll()
|
|
59
|
+
case "dist":
|
|
60
|
+
return dist()
|
|
61
|
+
case "smoke-test":
|
|
62
|
+
return smokeTest()
|
|
63
|
+
default:
|
|
64
|
+
return fmt.Errorf("unknown build target %q", flags.Arg(0))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func buildLocal() error {
|
|
69
|
+
return runGoBuild(localExecutable(), runtime.GOOS, runtime.GOARCH)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func clean() error {
|
|
73
|
+
for _, path := range []string{"savepoint", "savepoint.exe", "dist"} {
|
|
74
|
+
if err := os.RemoveAll(path); err != nil {
|
|
75
|
+
return fmt.Errorf("clean %s: %w", path, err)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return nil
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func buildMatching(goos string) error {
|
|
82
|
+
for _, target := range targets {
|
|
83
|
+
if target.os != goos {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if err := buildTarget(target); err != nil {
|
|
87
|
+
return err
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return nil
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func buildAll() error {
|
|
94
|
+
for _, target := range targets {
|
|
95
|
+
if err := buildTarget(target); err != nil {
|
|
96
|
+
return err
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return nil
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func buildTarget(target target) error {
|
|
103
|
+
output := filepath.Join("dist", target.os+"-"+target.arch, "savepoint")
|
|
104
|
+
return runGoBuild(output, target.os, target.arch)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func runGoBuild(output, goos, goarch string) error {
|
|
108
|
+
if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil && filepath.Dir(output) != "." {
|
|
109
|
+
return fmt.Errorf("create output dir: %w", err)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
cmd := exec.Command("go", "build", "-ldflags", "-X main.version="+version(), "-o", output, "main.go")
|
|
113
|
+
cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)
|
|
114
|
+
cmd.Stdout = os.Stdout
|
|
115
|
+
cmd.Stderr = os.Stderr
|
|
116
|
+
if err := cmd.Run(); err != nil {
|
|
117
|
+
return fmt.Errorf("build %s/%s: %w", goos, goarch, err)
|
|
118
|
+
}
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func dist() error {
|
|
123
|
+
if err := buildAll(); err != nil {
|
|
124
|
+
return err
|
|
125
|
+
}
|
|
126
|
+
for _, target := range targets {
|
|
127
|
+
name := "savepoint-" + version() + "-" + target.os + "-" + target.arch + ".tar.gz"
|
|
128
|
+
source := filepath.Join("dist", target.os+"-"+target.arch, "savepoint")
|
|
129
|
+
archive := filepath.Join("dist", name)
|
|
130
|
+
if err := writeTarGz(archive, source, "savepoint"); err != nil {
|
|
131
|
+
return err
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return nil
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func writeTarGz(archivePath, sourcePath, archiveName string) error {
|
|
138
|
+
source, err := os.Open(sourcePath)
|
|
139
|
+
if err != nil {
|
|
140
|
+
return fmt.Errorf("open artifact source: %w", err)
|
|
141
|
+
}
|
|
142
|
+
defer source.Close()
|
|
143
|
+
|
|
144
|
+
info, err := source.Stat()
|
|
145
|
+
if err != nil {
|
|
146
|
+
return fmt.Errorf("stat artifact source: %w", err)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
archive, err := os.Create(archivePath)
|
|
150
|
+
if err != nil {
|
|
151
|
+
return fmt.Errorf("create archive: %w", err)
|
|
152
|
+
}
|
|
153
|
+
defer archive.Close()
|
|
154
|
+
|
|
155
|
+
gzipWriter := gzip.NewWriter(archive)
|
|
156
|
+
defer gzipWriter.Close()
|
|
157
|
+
|
|
158
|
+
tarWriter := tar.NewWriter(gzipWriter)
|
|
159
|
+
defer tarWriter.Close()
|
|
160
|
+
|
|
161
|
+
header, err := tar.FileInfoHeader(info, "")
|
|
162
|
+
if err != nil {
|
|
163
|
+
return fmt.Errorf("create archive header: %w", err)
|
|
164
|
+
}
|
|
165
|
+
header.Name = archiveName
|
|
166
|
+
if err := tarWriter.WriteHeader(header); err != nil {
|
|
167
|
+
return fmt.Errorf("write archive header: %w", err)
|
|
168
|
+
}
|
|
169
|
+
if _, err := io.Copy(tarWriter, source); err != nil {
|
|
170
|
+
return fmt.Errorf("write archive content: %w", err)
|
|
171
|
+
}
|
|
172
|
+
return nil
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func smokeTest() error {
|
|
176
|
+
if err := buildLocal(); err != nil {
|
|
177
|
+
return err
|
|
178
|
+
}
|
|
179
|
+
cmd := exec.Command("."+string(os.PathSeparator)+localExecutable(), "--version")
|
|
180
|
+
cmd.Stdout = os.Stdout
|
|
181
|
+
cmd.Stderr = os.Stderr
|
|
182
|
+
if err := cmd.Run(); err != nil {
|
|
183
|
+
return fmt.Errorf("smoke test: %w", err)
|
|
184
|
+
}
|
|
185
|
+
fmt.Println("smoke test passed")
|
|
186
|
+
return nil
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func version() string {
|
|
190
|
+
if versionOverride != "" {
|
|
191
|
+
return versionOverride
|
|
192
|
+
}
|
|
193
|
+
if value := os.Getenv("VERSION"); value != "" {
|
|
194
|
+
return value
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
|
|
198
|
+
output, err := cmd.Output()
|
|
199
|
+
if err == nil && len(output) > 0 {
|
|
200
|
+
return strings.TrimSpace(string(output))
|
|
201
|
+
}
|
|
202
|
+
return "v0.0.0"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func localExecutable() string {
|
|
206
|
+
if runtime.GOOS == "windows" {
|
|
207
|
+
return "savepoint.exe"
|
|
208
|
+
}
|
|
209
|
+
return "savepoint"
|
|
210
|
+
}
|
|
211
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"runtime"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestVersion_override(t *testing.T) {
|
|
10
|
+
versionOverride = "v1.2.3"
|
|
11
|
+
defer func() { versionOverride = "" }()
|
|
12
|
+
if got := version(); got != "v1.2.3" {
|
|
13
|
+
t.Errorf("version() = %q, want %q", got, "v1.2.3")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func TestVersion_env(t *testing.T) {
|
|
18
|
+
versionOverride = ""
|
|
19
|
+
os.Setenv("VERSION", "v2.0.0-env")
|
|
20
|
+
defer os.Unsetenv("VERSION")
|
|
21
|
+
if got := version(); got != "v2.0.0-env" {
|
|
22
|
+
t.Errorf("version() = %q, want %q", got, "v2.0.0-env")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func TestVersion_fallback(t *testing.T) {
|
|
27
|
+
versionOverride = ""
|
|
28
|
+
os.Unsetenv("VERSION")
|
|
29
|
+
got := version()
|
|
30
|
+
if got == "" {
|
|
31
|
+
t.Error("version() returned empty string")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func TestLocalExecutable(t *testing.T) {
|
|
36
|
+
got := localExecutable()
|
|
37
|
+
if runtime.GOOS == "windows" {
|
|
38
|
+
if got != "savepoint.exe" {
|
|
39
|
+
t.Errorf("localExecutable() = %q, want %q", got, "savepoint.exe")
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
if got != "savepoint" {
|
|
43
|
+
t.Errorf("localExecutable() = %q, want %q", got, "savepoint")
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/internal/data/config.go
CHANGED
|
@@ -16,8 +16,17 @@ type Theme struct {
|
|
|
16
16
|
Accents map[string]string `yaml:"accents"`
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
type QualityGates struct {
|
|
20
|
+
Lint *string `yaml:"lint"`
|
|
21
|
+
Typecheck *string `yaml:"typecheck"`
|
|
22
|
+
Test *string `yaml:"test"`
|
|
23
|
+
BlockOnFailure bool `yaml:"block_on_failure"`
|
|
24
|
+
Timeout string `yaml:"gate_timeout"`
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
type Config struct {
|
|
20
|
-
Theme
|
|
28
|
+
Theme Theme `yaml:"theme"`
|
|
29
|
+
QualityGates QualityGates `yaml:"quality_gates"`
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
var defaultTheme = Theme{
|
|
@@ -80,8 +89,13 @@ func fillThemeDefaults(theme Theme) Theme {
|
|
|
80
89
|
if theme.Text == "" {
|
|
81
90
|
theme.Text = defaultTheme.Text
|
|
82
91
|
}
|
|
83
|
-
if
|
|
84
|
-
theme.Accents =
|
|
92
|
+
if theme.Accents == nil {
|
|
93
|
+
theme.Accents = make(map[string]string)
|
|
94
|
+
}
|
|
95
|
+
for k, v := range defaultTheme.Accents {
|
|
96
|
+
if _, ok := theme.Accents[k]; !ok {
|
|
97
|
+
theme.Accents[k] = v
|
|
98
|
+
}
|
|
85
99
|
}
|
|
86
100
|
return theme
|
|
87
101
|
}
|
|
@@ -53,6 +53,55 @@ func TestConfigReaderRead(t *testing.T) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
func TestFillThemeDefaults_PartialAccents(t *testing.T) {
|
|
57
|
+
theme := Theme{
|
|
58
|
+
BG: "#000000",
|
|
59
|
+
Accents: map[string]string{"planned": "#ff0000"},
|
|
60
|
+
}
|
|
61
|
+
result := fillThemeDefaults(theme)
|
|
62
|
+
if result.Accents["planned"] != "#ff0000" {
|
|
63
|
+
t.Errorf("Accents[planned] = %v, want #ff0000 (user value preserved)", result.Accents["planned"])
|
|
64
|
+
}
|
|
65
|
+
if result.Accents["in_progress"] != defaultTheme.Accents["in_progress"] {
|
|
66
|
+
t.Errorf("Accents[in_progress] = %v, want default %v", result.Accents["in_progress"], defaultTheme.Accents["in_progress"])
|
|
67
|
+
}
|
|
68
|
+
if result.Accents["done"] != defaultTheme.Accents["done"] {
|
|
69
|
+
t.Errorf("Accents[done] = %v, want default %v", result.Accents["done"], defaultTheme.Accents["done"])
|
|
70
|
+
}
|
|
71
|
+
if result.Accents["blocked"] != defaultTheme.Accents["blocked"] {
|
|
72
|
+
t.Errorf("Accents[blocked] = %v, want default %v", result.Accents["blocked"], defaultTheme.Accents["blocked"])
|
|
73
|
+
}
|
|
74
|
+
if result.Accents["epic"] != defaultTheme.Accents["epic"] {
|
|
75
|
+
t.Errorf("Accents[epic] = %v, want default %v", result.Accents["epic"], defaultTheme.Accents["epic"])
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func TestFillThemeDefaults_NilAccents(t *testing.T) {
|
|
80
|
+
theme := Theme{
|
|
81
|
+
BG: "#000000",
|
|
82
|
+
Accents: nil,
|
|
83
|
+
}
|
|
84
|
+
result := fillThemeDefaults(theme)
|
|
85
|
+
for k, v := range defaultTheme.Accents {
|
|
86
|
+
if result.Accents[k] != v {
|
|
87
|
+
t.Errorf("Accents[%s] = %v, want default %v", k, result.Accents[k], v)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func TestFillThemeDefaults_EmptyAccents(t *testing.T) {
|
|
93
|
+
theme := Theme{
|
|
94
|
+
BG: "#000000",
|
|
95
|
+
Accents: map[string]string{},
|
|
96
|
+
}
|
|
97
|
+
result := fillThemeDefaults(theme)
|
|
98
|
+
for k, v := range defaultTheme.Accents {
|
|
99
|
+
if result.Accents[k] != v {
|
|
100
|
+
t.Errorf("Accents[%s] = %v, want default %v", k, result.Accents[k], v)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
func TestConfigReaderMalformedYAML(t *testing.T) {
|
|
57
106
|
tmpfile, err := os.CreateTemp("", "config-*.yml")
|
|
58
107
|
if err != nil {
|
|
@@ -85,6 +85,32 @@ func (d *Discover) ListReleases(root string) ([]ReleaseInfo, error) {
|
|
|
85
85
|
return releases, nil
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// ListRootDirs returns sorted child directory names directly under root.
|
|
89
|
+
func (d *Discover) ListRootDirs(root string) ([]string, error) {
|
|
90
|
+
info, err := os.Stat(root)
|
|
91
|
+
if err != nil {
|
|
92
|
+
return nil, err
|
|
93
|
+
}
|
|
94
|
+
if !info.IsDir() {
|
|
95
|
+
return nil, fmt.Errorf("%s is not a directory", root)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
entries, err := os.ReadDir(root)
|
|
99
|
+
if err != nil {
|
|
100
|
+
return nil, err
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
var dirs []string
|
|
104
|
+
for _, entry := range entries {
|
|
105
|
+
if entry.IsDir() {
|
|
106
|
+
dirs = append(dirs, entry.Name())
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
sort.Strings(dirs)
|
|
111
|
+
return dirs, nil
|
|
112
|
+
}
|
|
113
|
+
|
|
88
114
|
func (d *Discover) ListEpics(root, release string) ([]EpicInfo, error) {
|
|
89
115
|
epicsPath := filepath.Join(root, "releases", release, "epics")
|
|
90
116
|
info, err := os.Stat(epicsPath)
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
package data
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"os"
|
|
5
4
|
"path/filepath"
|
|
6
5
|
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/opencode/savepoint/internal/testutil"
|
|
7
8
|
)
|
|
8
9
|
|
|
9
10
|
func TestFindSavepointRoot(t *testing.T) {
|
|
10
11
|
d := NewDiscover()
|
|
11
12
|
savepointRoot := createDiscoveryFixture(t)
|
|
12
13
|
start := filepath.Join(filepath.Dir(savepointRoot), "nested", "child")
|
|
13
|
-
|
|
14
|
-
t.Fatal(err)
|
|
15
|
-
}
|
|
14
|
+
testutil.MkdirAll(t, start)
|
|
16
15
|
|
|
17
16
|
root, err := d.FindSavepointRoot(start)
|
|
18
17
|
if err != nil {
|
|
@@ -40,6 +39,35 @@ func TestListReleases(t *testing.T) {
|
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
|
|
42
|
+
func TestListRootDirs(t *testing.T) {
|
|
43
|
+
d := NewDiscover()
|
|
44
|
+
root := t.TempDir()
|
|
45
|
+
testutil.MkdirAll(t, filepath.Join(root, "beta"))
|
|
46
|
+
testutil.MkdirAll(t, filepath.Join(root, "alpha"))
|
|
47
|
+
testutil.WriteFile(t, filepath.Join(root, "notes.txt"), "test")
|
|
48
|
+
|
|
49
|
+
dirs, err := d.ListRootDirs(root)
|
|
50
|
+
if err != nil {
|
|
51
|
+
t.Fatalf("ListRootDirs() error = %v", err)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if len(dirs) != 2 || dirs[0] != "alpha" || dirs[1] != "beta" {
|
|
55
|
+
t.Fatalf("ListRootDirs() = %v, want [alpha beta]", dirs)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func TestListRootDirsRejectsFile(t *testing.T) {
|
|
60
|
+
d := NewDiscover()
|
|
61
|
+
root := t.TempDir()
|
|
62
|
+
path := filepath.Join(root, "not-dir")
|
|
63
|
+
testutil.WriteFile(t, path, "test")
|
|
64
|
+
|
|
65
|
+
_, err := d.ListRootDirs(path)
|
|
66
|
+
if err == nil {
|
|
67
|
+
t.Fatal("ListRootDirs() error = nil, want not directory error")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
func TestListEpics(t *testing.T) {
|
|
44
72
|
d := NewDiscover()
|
|
45
73
|
root := createDiscoveryFixture(t)
|
|
@@ -86,9 +114,7 @@ func createDiscoveryFixture(t *testing.T) string {
|
|
|
86
114
|
filepath.Join(savepointRoot, "releases", "v2", "epics"),
|
|
87
115
|
}
|
|
88
116
|
for _, path := range paths {
|
|
89
|
-
|
|
90
|
-
t.Fatal(err)
|
|
91
|
-
}
|
|
117
|
+
testutil.MkdirAll(t, path)
|
|
92
118
|
}
|
|
93
119
|
|
|
94
120
|
files := []string{
|
|
@@ -97,9 +123,7 @@ func createDiscoveryFixture(t *testing.T) string {
|
|
|
97
123
|
filepath.Join(savepointRoot, "releases", "v1", "epics", "E02-data-readers", "tasks", "notes.txt"),
|
|
98
124
|
}
|
|
99
125
|
for _, file := range files {
|
|
100
|
-
|
|
101
|
-
t.Fatal(err)
|
|
102
|
-
}
|
|
126
|
+
testutil.WriteFile(t, file, "test")
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
return savepointRoot
|
package/internal/data/errors.go
CHANGED
|
@@ -6,4 +6,8 @@ var (
|
|
|
6
6
|
ErrNoFrontmatter = errors.New("no frontmatter found")
|
|
7
7
|
ErrNoClosingFrontmatter = errors.New("no closing frontmatter delimiter found")
|
|
8
8
|
ErrSavepointDirectoryMissing = errors.New(".savepoint directory not found")
|
|
9
|
+
ErrInvalidStatus = errors.New("invalid router state")
|
|
10
|
+
ErrMissingFrontmatter = errors.New("missing or invalid frontmatter")
|
|
11
|
+
ErrConfigNotFound = errors.New("configuration file not found")
|
|
12
|
+
ErrStructureProblem = errors.New("project structure problem")
|
|
9
13
|
)
|
|
@@ -2,17 +2,24 @@ package data
|
|
|
2
2
|
|
|
3
3
|
import "fmt"
|
|
4
4
|
|
|
5
|
-
func ValidateTaskLifecycle(task Task) error {
|
|
5
|
+
func ValidateTaskLifecycle(task *Task) error {
|
|
6
6
|
if !IsCanonicalColumn(task.Column) {
|
|
7
|
-
return fmt.Errorf("invalid
|
|
7
|
+
return fmt.Errorf("invalid status %q: use planned, in_progress, or done. Add 'status: planned' or 'status: in_progress' to task frontmatter", task.Column)
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
if task.Column
|
|
11
|
-
|
|
10
|
+
if task.Column == ColumnInProgress {
|
|
11
|
+
if task.Stage == "" {
|
|
12
|
+
task.Stage = StageBuild
|
|
13
|
+
return nil
|
|
14
|
+
}
|
|
15
|
+
if !IsCanonicalStage(task.Stage) {
|
|
16
|
+
return fmt.Errorf("invalid phase %q: use build, test, or audit. Add 'phase: build' to task frontmatter", task.Stage)
|
|
17
|
+
}
|
|
18
|
+
return nil
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
if task.
|
|
15
|
-
return fmt.Errorf("
|
|
21
|
+
if task.Stage != "" {
|
|
22
|
+
return fmt.Errorf("phase field %q is only valid when status is in_progress. Remove 'phase' or change status to in_progress", task.Stage)
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
return nil
|
|
@@ -4,35 +4,38 @@ import "testing"
|
|
|
4
4
|
|
|
5
5
|
func TestValidateTaskLifecycle_allowsPlannedWithoutPhase(t *testing.T) {
|
|
6
6
|
task := Task{Column: ColumnPlanned}
|
|
7
|
-
if err := ValidateTaskLifecycle(task); err != nil {
|
|
7
|
+
if err := ValidateTaskLifecycle(&task); err != nil {
|
|
8
8
|
t.Fatalf("ValidateTaskLifecycle() error = %v", err)
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
func TestValidateTaskLifecycle_defaultsInProgressWithoutPhase(t *testing.T) {
|
|
13
|
+
task := Task{Column: ColumnInProgress}
|
|
14
|
+
if err := ValidateTaskLifecycle(&task); err != nil {
|
|
15
|
+
t.Fatalf("ValidateTaskLifecycle() error = %v", err)
|
|
16
|
+
}
|
|
17
|
+
if task.Stage != StageBuild {
|
|
18
|
+
t.Fatalf("Task.Stage = %q, want %q", task.Stage, StageBuild)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
func TestValidateTaskLifecycle_allowsInProgressWithPhase(t *testing.T) {
|
|
13
23
|
task := Task{Column: ColumnInProgress, Stage: StageAudit}
|
|
14
|
-
if err := ValidateTaskLifecycle(task); err != nil {
|
|
24
|
+
if err := ValidateTaskLifecycle(&task); err != nil {
|
|
15
25
|
t.Fatalf("ValidateTaskLifecycle() error = %v", err)
|
|
16
26
|
}
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
func TestValidateTaskLifecycle_rejectsUnknownStatus(t *testing.T) {
|
|
20
30
|
task := Task{Column: "review"}
|
|
21
|
-
if err := ValidateTaskLifecycle(task); err == nil {
|
|
31
|
+
if err := ValidateTaskLifecycle(&task); err == nil {
|
|
22
32
|
t.Fatal("ValidateTaskLifecycle() expected unknown status error")
|
|
23
33
|
}
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
func TestValidateTaskLifecycle_rejectsPhaseOutsideInProgress(t *testing.T) {
|
|
27
37
|
task := Task{Column: ColumnPlanned, Stage: StageBuild}
|
|
28
|
-
if err := ValidateTaskLifecycle(task); err == nil {
|
|
38
|
+
if err := ValidateTaskLifecycle(&task); err == nil {
|
|
29
39
|
t.Fatal("ValidateTaskLifecycle() expected phase/status error")
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
|
-
|
|
33
|
-
func TestValidateTaskLifecycle_rejectsInProgressWithoutCanonicalPhase(t *testing.T) {
|
|
34
|
-
task := Task{Column: ColumnInProgress}
|
|
35
|
-
if err := ValidateTaskLifecycle(task); err == nil {
|
|
36
|
-
t.Fatal("ValidateTaskLifecycle() expected missing phase error")
|
|
37
|
-
}
|
|
38
|
-
}
|
package/internal/data/parser.go
CHANGED
|
@@ -46,7 +46,7 @@ func (p *Parser) ParseTaskFile(path string, content string) (*Task, error) {
|
|
|
46
46
|
Epic: firstNonEmpty(fields.Epic, extractEpicFromID(fields.ID)),
|
|
47
47
|
Release: firstNonEmpty(fields.Release, "v1"),
|
|
48
48
|
Column: normalizeColumn(rawColumn),
|
|
49
|
-
Stage: firstStage(fields.
|
|
49
|
+
Stage: firstStage(fields.Phase, fields.Stage),
|
|
50
50
|
Priority: fields.Priority,
|
|
51
51
|
Points: fields.Points,
|
|
52
52
|
Tags: fields.Tags,
|
|
@@ -61,6 +61,10 @@ func (p *Parser) ParseTaskFile(path string, content string) (*Task, error) {
|
|
|
61
61
|
return nil, fmt.Errorf("parse error for %s: %w", path, err)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
if task.Column == ColumnInProgress && task.Stage == "" {
|
|
65
|
+
task.Stage = StageBuild
|
|
66
|
+
}
|
|
67
|
+
|
|
64
68
|
return task, nil
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -84,8 +88,13 @@ type taskFrontmatter struct {
|
|
|
84
88
|
Progress Progress `yaml:"progress"`
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
// normalizeLineEndings replaces Windows line endings with Unix line endings.
|
|
92
|
+
func normalizeLineEndings(s string) string {
|
|
93
|
+
return strings.ReplaceAll(s, "\r\n", "\n")
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
func extractFrontmatter(content string) (string, error) {
|
|
88
|
-
normalized :=
|
|
97
|
+
normalized := normalizeLineEndings(content)
|
|
89
98
|
if !strings.HasPrefix(normalized, "---\n") {
|
|
90
99
|
return "", ErrNoFrontmatter
|
|
91
100
|
}
|
|
@@ -139,9 +148,15 @@ const legacyTodoColumn ColumnType = "todo"
|
|
|
139
148
|
|
|
140
149
|
func validateParsedTaskLifecycle(rawColumn ColumnType, task Task) error {
|
|
141
150
|
if rawColumn != "" && rawColumn != legacyTodoColumn && !IsCanonicalColumn(rawColumn) {
|
|
142
|
-
return fmt.Errorf("invalid task status %q: use planned, in_progress, or done", rawColumn)
|
|
151
|
+
return fmt.Errorf("invalid task status %q: use planned, in_progress, or done. Add 'status: planned' or 'status: in_progress' to task frontmatter", rawColumn)
|
|
152
|
+
}
|
|
153
|
+
if task.Column == ColumnInProgress && !IsCanonicalStage(task.Stage) && task.Stage != "" {
|
|
154
|
+
return fmt.Errorf("invalid phase %q: use build, test, or audit. Add 'phase: build' to task frontmatter", task.Stage)
|
|
143
155
|
}
|
|
144
|
-
|
|
156
|
+
if task.Column != ColumnInProgress && task.Stage != "" {
|
|
157
|
+
return nil
|
|
158
|
+
}
|
|
159
|
+
return nil
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
func firstStage(values ...ProgressStage) ProgressStage {
|
|
@@ -163,7 +178,7 @@ func firstList(values ...[]string) []string {
|
|
|
163
178
|
}
|
|
164
179
|
|
|
165
180
|
func extractChecklistItems(content, heading string) []CheckItem {
|
|
166
|
-
normalized :=
|
|
181
|
+
normalized := normalizeLineEndings(content)
|
|
167
182
|
start := strings.Index(normalized, heading)
|
|
168
183
|
if start == -1 {
|
|
169
184
|
return nil
|
|
@@ -175,25 +190,33 @@ func extractChecklistItems(content, heading string) []CheckItem {
|
|
|
175
190
|
}
|
|
176
191
|
|
|
177
192
|
items := []CheckItem{}
|
|
193
|
+
var current *CheckItem
|
|
178
194
|
for _, line := range strings.Split(section, "\n") {
|
|
179
195
|
trimmed := strings.TrimSpace(line)
|
|
180
196
|
if strings.HasPrefix(trimmed, "- [x] ") {
|
|
181
197
|
items = append(items, CheckItem{Text: strings.TrimSpace(trimmed[6:]), Done: true})
|
|
198
|
+
current = &items[len(items)-1]
|
|
182
199
|
continue
|
|
183
200
|
}
|
|
184
201
|
if strings.HasPrefix(trimmed, "- [ ] ") {
|
|
185
202
|
items = append(items, CheckItem{Text: strings.TrimSpace(trimmed[6:]), Done: false})
|
|
203
|
+
current = &items[len(items)-1]
|
|
186
204
|
continue
|
|
187
205
|
}
|
|
188
206
|
if strings.HasPrefix(trimmed, "- ") {
|
|
189
207
|
items = append(items, CheckItem{Text: strings.TrimSpace(trimmed[2:]), Done: false})
|
|
208
|
+
current = &items[len(items)-1]
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
if trimmed != "" && current != nil {
|
|
212
|
+
current.Text = strings.TrimSpace(current.Text + " " + trimmed)
|
|
190
213
|
}
|
|
191
214
|
}
|
|
192
215
|
return items
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
func extractChecklistSection(content, heading string) []string {
|
|
196
|
-
normalized :=
|
|
219
|
+
normalized := normalizeLineEndings(content)
|
|
197
220
|
start := strings.Index(normalized, heading)
|
|
198
221
|
if start == -1 {
|
|
199
222
|
return nil
|