savepoint 1.0.2 → 1.0.4
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 +12 -1
- package/.github/workflows/ci.yml +20 -0
- package/.golangci.yml +11 -0
- package/.savepoint/Design.md +40 -38
- package/.savepoint/{audit/v1.1/E02-cross-platform-compatibility/proposals.md → releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md} +48 -38
- 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 +14 -1
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +3 -3
- package/.savepoint/{audit/v1.1/E04-epic-navigation/proposals.md → releases/v1.1/epics/E04-epic-navigation/E04-Audit.md} +65 -54
- 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 +25 -16
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +17 -6
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +15 -5
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +19 -5
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +11 -1
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +9 -6
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +29 -13
- 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-Audit.md +272 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +60 -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 +34 -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 +33 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T008-ci-and-release-automation.md +46 -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 +67 -7
- package/.savepoint/router.md +10 -17
- package/AGENTS.md +39 -24
- package/Makefile +3 -1
- package/README.md +0 -1
- package/agent-skills/savepoint-audit/SKILL.md +86 -34
- package/agent-skills/savepoint-build-task/SKILL.md +7 -2
- package/agent-skills/savepoint-create-plan/SKILL.md +7 -2
- package/agent-skills/savepoint-create-task/SKILL.md +44 -31
- package/agent-skills/savepoint-draft-prd/SKILL.md +7 -2
- package/agent-skills/savepoint-system-design/SKILL.md +7 -2
- 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 +44 -36
- package/internal/board/board_test.go +27 -82
- package/internal/board/card.go +43 -23
- package/internal/board/card_test.go +74 -5
- package/internal/board/column.go +75 -15
- package/internal/board/column_test.go +76 -2
- package/internal/board/debug.go +26 -0
- package/internal/board/debug_test.go +108 -0
- package/internal/board/detail.go +33 -47
- package/internal/board/detail_test.go +48 -0
- package/internal/board/epic_panel.go +120 -22
- package/internal/board/epic_panel_test.go +302 -17
- 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/model.go +79 -118
- 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/status.go +4 -4
- 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 +344 -215
- package/internal/board/update_test.go +326 -18
- package/internal/board/util.go +76 -0
- package/internal/board/view.go +31 -28
- package/internal/board/view_test.go +74 -2
- package/internal/board/watch.go +41 -5
- package/internal/buildtool/main.go +45 -15
- package/internal/buildtool/main_test.go +224 -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/fuzz_test.go +75 -0
- package/internal/data/lifecycle.go +13 -6
- package/internal/data/lifecycle_test.go +14 -11
- package/internal/data/parser.go +22 -6
- package/internal/data/parser_test.go +31 -7
- package/internal/data/task.go +0 -9
- package/internal/data/testdata/fuzz/FuzzSplitFrontmatterBody/68eb66b0fe91e7e3 +2 -0
- package/internal/data/write.go +88 -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/styles_test.go +133 -0
- package/internal/testutil/fixture.go +113 -0
- package/internal/testutil/fs.go +26 -0
- package/main.go +120 -4
- package/package.json +2 -2
- package/project-audit/audit_report_glm_5.1.md +411 -0
- package/project-audit/audit_report_opus_4.6.md +406 -0
- package/project-audit/consolidated-audit-report.md +456 -0
- package/templates/project/.savepoint/Design.md +2 -2
- package/templates/project/.savepoint/router.md +10 -10
- package/templates/project/AGENTS.md +33 -21
- 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 +33 -28
- package/templates/prompts/design.prompt.md +3 -1
- package/.savepoint/audit/v1/E01/proposals.md +0 -168
- package/.savepoint/audit/v1/E01/snapshot.md +0 -78
- package/.savepoint/audit/v1/E01-go-setup/proposals.md +0 -166
- package/.savepoint/audit/v1/E01-go-setup/snapshot.md +0 -71
- package/.savepoint/audit/v1/E01-scaffolding/proposals/AGENTS.md +0 -66
- package/.savepoint/audit/v1/E01-scaffolding/proposals/Design.md +0 -210
- package/.savepoint/audit/v1/E01-scaffolding/proposals/epic-Design.md +0 -117
- package/.savepoint/audit/v1/E01-scaffolding/proposals/quality-review.md +0 -101
- package/.savepoint/audit/v1/E01-scaffolding/snapshot.md +0 -54
- package/.savepoint/audit/v1/E02-data-model/snapshot.md +0 -128
- package/.savepoint/audit/v1/E02-data-readers/proposals.md +0 -123
- package/.savepoint/audit/v1/E02-data-readers/snapshot.md +0 -54
- package/.savepoint/audit/v1/E03-board-tui-core/proposals.md +0 -146
- package/.savepoint/audit/v1/E03-board-tui-core/snapshot.md +0 -57
- package/.savepoint/audit/v1/E03-cli-foundation/snapshot.md +0 -106
- package/.savepoint/audit/v1/E04-board-components/proposals.md +0 -118
- package/.savepoint/audit/v1/E04-board-components/snapshot.md +0 -77
- package/.savepoint/audit/v1/E04-templates-and-prompts/snapshot.md +0 -115
- package/.savepoint/audit/v1/E05-init-command/snapshot.md +0 -125
- package/.savepoint/audit/v1/E05-phase-transitions/proposals.md +0 -83
- package/.savepoint/audit/v1/E05-phase-transitions/snapshot.md +0 -36
- package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +0 -130
- package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +0 -84
- package/.savepoint/audit/v1/E06-tui-board/snapshot.md +0 -64
- package/.savepoint/audit/v1/E07-audit-pipeline/snapshot.md +0 -165
- package/.savepoint/audit/v1/E08-board-workflow-cleanup/snapshot.md +0 -65
- package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +0 -41
- package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +0 -48
- package/ink-cli-ui-design.zip +0 -0
- package/savepoint +0 -0
- package/savepoint.exe +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import "github.com/opencode/savepoint/internal/data"
|
|
4
|
+
|
|
5
|
+
// taskDiscoverer provides project traversal for board loading.
|
|
6
|
+
type taskDiscoverer interface {
|
|
7
|
+
FindSavepointRoot(start string) (string, error)
|
|
8
|
+
ListReleases(root string) ([]data.ReleaseInfo, error)
|
|
9
|
+
ListEpics(root, release string) ([]data.EpicInfo, error)
|
|
10
|
+
ListTasks(root, release, epic string) ([]data.TaskInfo, error)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// taskParser parses Savepoint frontmatter and task files for board loading.
|
|
14
|
+
type taskParser interface {
|
|
15
|
+
ParseFrontmatter(content string) (map[string]any, error)
|
|
16
|
+
ParseTaskFile(path string, content string) (*data.Task, error)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// configReader reads board display configuration.
|
|
20
|
+
type configReader interface {
|
|
21
|
+
Read(path string) (*data.Config, error)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// routerReader parses router state from router.md content.
|
|
25
|
+
type routerReader interface {
|
|
26
|
+
ReadState(content string) (*data.RouterState, error)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ModelDependencies contains board data-access dependencies.
|
|
30
|
+
type ModelDependencies struct {
|
|
31
|
+
Discoverer taskDiscoverer
|
|
32
|
+
Parser taskParser
|
|
33
|
+
ConfigReader configReader
|
|
34
|
+
RouterReader routerReader
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func defaultModelDependencies() ModelDependencies {
|
|
38
|
+
return ModelDependencies{
|
|
39
|
+
Discoverer: data.NewDiscover(),
|
|
40
|
+
Parser: data.NewParser(),
|
|
41
|
+
ConfigReader: data.NewConfigReader(),
|
|
42
|
+
RouterReader: data.NewRouterReader(),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func modelDependencies(overrides []ModelDependencies) ModelDependencies {
|
|
47
|
+
deps := defaultModelDependencies()
|
|
48
|
+
if len(overrides) == 0 {
|
|
49
|
+
return deps
|
|
50
|
+
}
|
|
51
|
+
override := overrides[0]
|
|
52
|
+
if override.Discoverer != nil {
|
|
53
|
+
deps.Discoverer = override.Discoverer
|
|
54
|
+
}
|
|
55
|
+
if override.Parser != nil {
|
|
56
|
+
deps.Parser = override.Parser
|
|
57
|
+
}
|
|
58
|
+
if override.ConfigReader != nil {
|
|
59
|
+
deps.ConfigReader = override.ConfigReader
|
|
60
|
+
}
|
|
61
|
+
if override.RouterReader != nil {
|
|
62
|
+
deps.RouterReader = override.RouterReader
|
|
63
|
+
}
|
|
64
|
+
return deps
|
|
65
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/opencode/savepoint/internal/data"
|
|
8
|
+
"github.com/opencode/savepoint/internal/testutil"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
type stubBoardDiscoverer struct {
|
|
12
|
+
root string
|
|
13
|
+
releases []data.ReleaseInfo
|
|
14
|
+
epics map[string][]data.EpicInfo
|
|
15
|
+
tasks map[string][]data.TaskInfo
|
|
16
|
+
findCalls int
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func (d *stubBoardDiscoverer) FindSavepointRoot(start string) (string, error) {
|
|
20
|
+
d.findCalls++
|
|
21
|
+
return d.root, nil
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func (d *stubBoardDiscoverer) ListReleases(root string) ([]data.ReleaseInfo, error) {
|
|
25
|
+
return d.releases, nil
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func (d *stubBoardDiscoverer) ListEpics(root, release string) ([]data.EpicInfo, error) {
|
|
29
|
+
return d.epics[release], nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func (d *stubBoardDiscoverer) ListTasks(root, release, epic string) ([]data.TaskInfo, error) {
|
|
33
|
+
return d.tasks[release+"/"+epic], nil
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type countingBoardParser struct {
|
|
37
|
+
parser *data.Parser
|
|
38
|
+
frontmatterCalls int
|
|
39
|
+
taskFileCalls int
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func (p *countingBoardParser) ParseFrontmatter(content string) (map[string]any, error) {
|
|
43
|
+
p.frontmatterCalls++
|
|
44
|
+
return p.parser.ParseFrontmatter(content)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func (p *countingBoardParser) ParseTaskFile(path string, content string) (*data.Task, error) {
|
|
48
|
+
p.taskFileCalls++
|
|
49
|
+
return p.parser.ParseTaskFile(path, content)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type stubBoardRouterReader struct {
|
|
53
|
+
state *data.RouterState
|
|
54
|
+
calls int
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func (r *stubBoardRouterReader) ReadState(content string) (*data.RouterState, error) {
|
|
58
|
+
r.calls++
|
|
59
|
+
return r.state, nil
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func TestNewProjectModelUsesInjectedInterfaces(t *testing.T) {
|
|
63
|
+
projectRoot := t.TempDir()
|
|
64
|
+
savepointRoot := filepath.Join(projectRoot, ".savepoint")
|
|
65
|
+
epicPath := filepath.Join(savepointRoot, "releases", "v9", "epics", "E01-mock")
|
|
66
|
+
taskPath := filepath.Join(epicPath, "tasks", "T001-mock.md")
|
|
67
|
+
|
|
68
|
+
testutil.WriteFile(t, filepath.Join(savepointRoot, "router.md"), "# router")
|
|
69
|
+
testutil.WriteFile(t, filepath.Join(epicPath, "E01-Detail.md"), "---\ntype: epic-design\nstatus: planned\n---\n\n# Epic\n")
|
|
70
|
+
testutil.WriteFile(t, taskPath, "---\nid: E01-mock/T001-mock\nstatus: planned\nobjective: Mock task\ndepends_on: []\n---\n\n# Task\n")
|
|
71
|
+
|
|
72
|
+
discoverer := &stubBoardDiscoverer{
|
|
73
|
+
root: savepointRoot,
|
|
74
|
+
releases: []data.ReleaseInfo{{
|
|
75
|
+
ID: "v9",
|
|
76
|
+
Path: filepath.Join(savepointRoot, "releases", "v9"),
|
|
77
|
+
}},
|
|
78
|
+
epics: map[string][]data.EpicInfo{
|
|
79
|
+
"v9": {{ID: "E01-mock", Path: epicPath}},
|
|
80
|
+
},
|
|
81
|
+
tasks: map[string][]data.TaskInfo{
|
|
82
|
+
"v9/E01-mock": {{ID: "T001-mock", Path: taskPath}},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
parser := &countingBoardParser{parser: data.NewParser()}
|
|
86
|
+
router := &stubBoardRouterReader{state: &data.RouterState{
|
|
87
|
+
State: "task-building",
|
|
88
|
+
Release: "v9",
|
|
89
|
+
Epic: "E01-mock",
|
|
90
|
+
Task: "E01-mock/T001-mock",
|
|
91
|
+
}}
|
|
92
|
+
|
|
93
|
+
model, err := newProjectModelWithDependencies(projectRoot, "", "", ModelDependencies{
|
|
94
|
+
Discoverer: discoverer,
|
|
95
|
+
Parser: parser,
|
|
96
|
+
RouterReader: router,
|
|
97
|
+
})
|
|
98
|
+
if err != nil {
|
|
99
|
+
t.Fatalf("newProjectModelWithDependencies() error = %v", err)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if discoverer.findCalls != 1 {
|
|
103
|
+
t.Fatalf("FindSavepointRoot calls = %d, want 1", discoverer.findCalls)
|
|
104
|
+
}
|
|
105
|
+
if router.calls != 1 {
|
|
106
|
+
t.Fatalf("ReadState calls = %d, want 1", router.calls)
|
|
107
|
+
}
|
|
108
|
+
if parser.frontmatterCalls != 1 || parser.taskFileCalls != 1 {
|
|
109
|
+
t.Fatalf("parser calls = frontmatter:%d task:%d, want 1 each", parser.frontmatterCalls, parser.taskFileCalls)
|
|
110
|
+
}
|
|
111
|
+
if got := model.Tasks[data.ColumnPlanned][0].ID; got != "E01-mock/T001-mock" {
|
|
112
|
+
t.Fatalf("loaded task = %q, want injected task", got)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"time"
|
|
7
|
+
|
|
8
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
9
|
+
"github.com/opencode/savepoint/internal/data"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func writeRouterTaskCmd(root string, task data.Task, reader routerReader) tea.Cmd {
|
|
13
|
+
return func() tea.Msg {
|
|
14
|
+
routerPath := filepath.Join(root, "router.md")
|
|
15
|
+
fi, err := os.Stat(routerPath)
|
|
16
|
+
if err != nil {
|
|
17
|
+
return errorMsg{message: err.Error()}
|
|
18
|
+
}
|
|
19
|
+
content, err := os.ReadFile(routerPath)
|
|
20
|
+
if err != nil {
|
|
21
|
+
return errorMsg{message: err.Error()}
|
|
22
|
+
}
|
|
23
|
+
state, err := reader.ReadState(string(content))
|
|
24
|
+
if err != nil {
|
|
25
|
+
return errorMsg{message: err.Error()}
|
|
26
|
+
}
|
|
27
|
+
state.Release = task.Release
|
|
28
|
+
state.Epic = task.Epic
|
|
29
|
+
state.State = "task-building"
|
|
30
|
+
state.Task = task.ID
|
|
31
|
+
state.NextAction = "Build " + task.ID + "."
|
|
32
|
+
if err := data.WriteRouterState(root, state, fi.ModTime()); err != nil {
|
|
33
|
+
return errorMsg{message: err.Error()}
|
|
34
|
+
}
|
|
35
|
+
message := "Router set to " + task.Release + " " + task.Epic + "/" + shortID(task.ID)
|
|
36
|
+
return routerWriteMsg{message: message, state: state, taskID: task.ID}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func writeRouterReleaseEpicCmd(root, selectedEpic, selectedRelease string, reader routerReader) tea.Cmd {
|
|
41
|
+
return func() tea.Msg {
|
|
42
|
+
routerPath := filepath.Join(root, "router.md")
|
|
43
|
+
fi, err := os.Stat(routerPath)
|
|
44
|
+
if err != nil {
|
|
45
|
+
return errorMsg{message: err.Error()}
|
|
46
|
+
}
|
|
47
|
+
content, err := os.ReadFile(routerPath)
|
|
48
|
+
if err != nil {
|
|
49
|
+
return errorMsg{message: err.Error()}
|
|
50
|
+
}
|
|
51
|
+
state, err := reader.ReadState(string(content))
|
|
52
|
+
if err != nil {
|
|
53
|
+
return errorMsg{message: err.Error()}
|
|
54
|
+
}
|
|
55
|
+
state.Epic = shortID(selectedEpic)
|
|
56
|
+
state.Release = selectedRelease
|
|
57
|
+
if err := data.WriteRouterState(root, state, fi.ModTime()); err != nil {
|
|
58
|
+
return errorMsg{message: err.Error()}
|
|
59
|
+
}
|
|
60
|
+
return routerWriteMsg{state: state}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func writeTaskStatusCmd(orig, next data.Task, expectedMtime time.Time, prefix string) tea.Cmd {
|
|
65
|
+
return func() tea.Msg {
|
|
66
|
+
if err := data.WriteTaskStatus(next.Path, &next, expectedMtime); err != nil {
|
|
67
|
+
return errorMsg{message: taskWriteErrorMessage(err)}
|
|
68
|
+
}
|
|
69
|
+
fi, err := os.Stat(next.Path)
|
|
70
|
+
if err != nil {
|
|
71
|
+
return errorMsg{message: err.Error()}
|
|
72
|
+
}
|
|
73
|
+
next.Mtime = fi.ModTime()
|
|
74
|
+
return taskWriteMsg{prefix: prefix, next: next}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func readEpicDetailCmd(epicDir, shortIDStr string) tea.Cmd {
|
|
79
|
+
return func() tea.Msg {
|
|
80
|
+
content := readEpicDetailFile(epicDir, shortIDStr)
|
|
81
|
+
return epicDetailMsg{content: content}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func readEpicAuditCmd(epicDir, shortIDStr string) tea.Cmd {
|
|
86
|
+
return func() tea.Msg {
|
|
87
|
+
raw, err := os.ReadFile(filepath.Join(epicDir, shortIDStr+"-Audit.md"))
|
|
88
|
+
if err != nil {
|
|
89
|
+
return auditContentMsg{content: "(no audit available)"}
|
|
90
|
+
}
|
|
91
|
+
return auditContentMsg{content: string(raw)}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/internal/board/model.go
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
package board
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"os"
|
|
5
|
-
"path/filepath"
|
|
6
|
-
|
|
7
4
|
tea "github.com/charmbracelet/bubbletea"
|
|
8
5
|
"github.com/fsnotify/fsnotify"
|
|
9
6
|
"github.com/opencode/savepoint/internal/data"
|
|
@@ -20,46 +17,94 @@ const (
|
|
|
20
17
|
OverlayEpicDetail OverlayType = "detail-epic"
|
|
21
18
|
)
|
|
22
19
|
|
|
23
|
-
//
|
|
24
|
-
type
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
// ViewConfig holds terminal and overlay presentation state.
|
|
21
|
+
type ViewConfig struct {
|
|
22
|
+
Theme data.Theme
|
|
23
|
+
Overlay OverlayType
|
|
24
|
+
Width int
|
|
25
|
+
Height int
|
|
26
|
+
StatusMessage string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// DataState holds task, router, and filesystem state used by the board.
|
|
30
|
+
type DataState struct {
|
|
31
|
+
AllTasks []data.Task
|
|
32
|
+
Tasks map[data.ColumnType][]data.Task
|
|
33
|
+
Root string
|
|
34
|
+
EpicStatus map[string]string
|
|
35
|
+
RouterTask string
|
|
36
|
+
RouterState *data.RouterState
|
|
37
|
+
Watcher *fsnotify.Watcher
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// NavigationState holds board-column and detail scrolling state.
|
|
41
|
+
type NavigationState struct {
|
|
42
|
+
FocusedColumn data.ColumnType
|
|
43
|
+
FocusedTask int
|
|
44
|
+
ColumnOffsets map[data.ColumnType]int
|
|
45
|
+
DetailOffset int
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// EpicState holds epic list, sidebar, and detail overlay state.
|
|
49
|
+
type EpicState struct {
|
|
31
50
|
SelectedEpic string
|
|
32
|
-
SelectedRelease string
|
|
33
51
|
Epics []string
|
|
34
52
|
EpicCursor int
|
|
35
53
|
EpicPanelFocus bool
|
|
36
54
|
EpicPanelCursor int
|
|
37
55
|
EpicDetailOffset int
|
|
56
|
+
EpicDetailEpic string
|
|
38
57
|
EpicDetailContent string
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
EpicDetailTab int // 0=Detail, 1=Audit
|
|
59
|
+
EpicAuditContent string // cached E##-Audit.md content
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ReleaseState holds release list and release picker state.
|
|
63
|
+
type ReleaseState struct {
|
|
64
|
+
SelectedRelease string
|
|
65
|
+
Releases []string
|
|
66
|
+
ReleaseEpics map[string][]string
|
|
67
|
+
ReleaseCursor int
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// DataAccessState holds board data-access implementations.
|
|
71
|
+
type DataAccessState struct {
|
|
72
|
+
Dependencies ModelDependencies
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Model holds all board state. Tasks are grouped by column for O(1) column access.
|
|
76
|
+
type Model struct {
|
|
77
|
+
ViewConfig
|
|
78
|
+
DataState
|
|
79
|
+
NavigationState
|
|
80
|
+
EpicState
|
|
81
|
+
ReleaseState
|
|
82
|
+
DataAccessState
|
|
51
83
|
}
|
|
52
84
|
|
|
53
85
|
// NewModel groups tasks by column and returns an initialized Model.
|
|
54
|
-
func NewModel(tasks []data.Task, release, epic string) Model {
|
|
86
|
+
func NewModel(tasks []data.Task, release, epic string, deps ...ModelDependencies) Model {
|
|
55
87
|
m := Model{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
ViewConfig: ViewConfig{
|
|
89
|
+
Overlay: OverlayNone,
|
|
90
|
+
},
|
|
91
|
+
DataState: DataState{
|
|
92
|
+
AllTasks: append([]data.Task(nil), tasks...),
|
|
93
|
+
},
|
|
94
|
+
NavigationState: NavigationState{
|
|
95
|
+
FocusedColumn: data.ColumnPlanned,
|
|
96
|
+
FocusedTask: 0,
|
|
97
|
+
ColumnOffsets: newColumnOffsets(),
|
|
98
|
+
},
|
|
99
|
+
EpicState: EpicState{
|
|
100
|
+
SelectedEpic: epic,
|
|
101
|
+
},
|
|
102
|
+
ReleaseState: ReleaseState{
|
|
103
|
+
SelectedRelease: release,
|
|
104
|
+
},
|
|
105
|
+
DataAccessState: DataAccessState{
|
|
106
|
+
Dependencies: modelDependencies(deps),
|
|
107
|
+
},
|
|
63
108
|
}
|
|
64
109
|
m.refreshTasks()
|
|
65
110
|
return m
|
|
@@ -129,7 +174,7 @@ func (m *Model) refreshEpicsForRelease() {
|
|
|
129
174
|
|
|
130
175
|
for _, epic := range m.Epics {
|
|
131
176
|
if epic == m.SelectedEpic {
|
|
132
|
-
m.EpicCursor =
|
|
177
|
+
m.EpicCursor = sliceIndex(m.Epics, m.SelectedEpic)
|
|
133
178
|
m.clampEpicPanelCursor()
|
|
134
179
|
return
|
|
135
180
|
}
|
|
@@ -185,90 +230,6 @@ func (m *Model) clampColumnOffsets() {
|
|
|
185
230
|
}
|
|
186
231
|
}
|
|
187
232
|
|
|
188
|
-
func (m *Model) writeRouterReleaseEpic() error {
|
|
189
|
-
routerPath := filepath.Join(m.Root, "router.md")
|
|
190
|
-
|
|
191
|
-
fi, err := os.Stat(routerPath)
|
|
192
|
-
if err != nil {
|
|
193
|
-
return err
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
content, err := os.ReadFile(routerPath)
|
|
197
|
-
if err != nil {
|
|
198
|
-
return err
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
r := data.NewRouterReader()
|
|
202
|
-
state, err := r.ReadState(string(content))
|
|
203
|
-
if err != nil {
|
|
204
|
-
return err
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
state.Epic = shortID(m.SelectedEpic)
|
|
208
|
-
state.Release = m.SelectedRelease
|
|
209
|
-
|
|
210
|
-
return data.WriteRouterState(m.Root, state, fi.ModTime())
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
func (m *Model) writeRouterTask(task data.Task) (string, error) {
|
|
214
|
-
routerPath := filepath.Join(m.Root, "router.md")
|
|
215
|
-
|
|
216
|
-
fi, err := os.Stat(routerPath)
|
|
217
|
-
if err != nil {
|
|
218
|
-
return "", err
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
content, err := os.ReadFile(routerPath)
|
|
222
|
-
if err != nil {
|
|
223
|
-
return "", err
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
r := data.NewRouterReader()
|
|
227
|
-
state, err := r.ReadState(string(content))
|
|
228
|
-
if err != nil {
|
|
229
|
-
return "", err
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
state.Release = task.Release
|
|
233
|
-
state.Epic = task.Epic
|
|
234
|
-
if m.isLastUncompletedTask(task) {
|
|
235
|
-
state.State = "audit-pending"
|
|
236
|
-
state.Task = ""
|
|
237
|
-
state.NextAction = "Audit " + task.Epic + "."
|
|
238
|
-
if err := data.WriteRouterState(m.Root, state, fi.ModTime()); err != nil {
|
|
239
|
-
return "", err
|
|
240
|
-
}
|
|
241
|
-
m.RouterState = state
|
|
242
|
-
m.RouterTask = ""
|
|
243
|
-
return "Audit pending for " + task.Epic, nil
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
state.State = "task-building"
|
|
247
|
-
state.Task = task.ID
|
|
248
|
-
state.NextAction = "Build " + task.ID + "."
|
|
249
|
-
if err := data.WriteRouterState(m.Root, state, fi.ModTime()); err != nil {
|
|
250
|
-
return "", err
|
|
251
|
-
}
|
|
252
|
-
m.RouterState = state
|
|
253
|
-
m.RouterTask = task.ID
|
|
254
|
-
return "Router set to " + task.Release + " " + task.Epic + "/" + shortID(task.ID), nil
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
func (m Model) isLastUncompletedTask(task data.Task) bool {
|
|
258
|
-
for _, candidate := range m.AllTasks {
|
|
259
|
-
if candidate.ID == task.ID {
|
|
260
|
-
continue
|
|
261
|
-
}
|
|
262
|
-
if candidate.Release != task.Release || candidate.Epic != task.Epic {
|
|
263
|
-
continue
|
|
264
|
-
}
|
|
265
|
-
if !taskDone(candidate) {
|
|
266
|
-
return false
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return true
|
|
270
|
-
}
|
|
271
|
-
|
|
272
233
|
func taskDone(task data.Task) bool {
|
|
273
|
-
return task.Column == data.ColumnDone
|
|
234
|
+
return task.Column == data.ColumnDone
|
|
274
235
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/opencode/savepoint/internal/data"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const plainNonTTYWarning = "[non-interactive mode — run in a TTY to launch the board UI]"
|
|
13
|
+
const plainAuditSignal = "[◆ audit proposals pending]"
|
|
14
|
+
|
|
15
|
+
// RenderPlainTable renders a plain text three-column task table for non-TTY output.
|
|
16
|
+
func RenderPlainTable(model Model) string {
|
|
17
|
+
var b strings.Builder
|
|
18
|
+
|
|
19
|
+
fmt.Fprintln(&b, plainNonTTYWarning)
|
|
20
|
+
if hasAuditProposals(model.Root) {
|
|
21
|
+
fmt.Fprintln(&b, plainAuditSignal)
|
|
22
|
+
}
|
|
23
|
+
fmt.Fprintln(&b)
|
|
24
|
+
|
|
25
|
+
cols := []struct {
|
|
26
|
+
label string
|
|
27
|
+
col data.ColumnType
|
|
28
|
+
}{
|
|
29
|
+
{"PLANNED", data.ColumnPlanned},
|
|
30
|
+
{"IN PROGRESS", data.ColumnInProgress},
|
|
31
|
+
{"DONE", data.ColumnDone},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for _, c := range cols {
|
|
35
|
+
tasks := model.Tasks[c.col]
|
|
36
|
+
fmt.Fprintln(&b, c.label)
|
|
37
|
+
if len(tasks) == 0 {
|
|
38
|
+
fmt.Fprintln(&b, " (none)")
|
|
39
|
+
}
|
|
40
|
+
for _, t := range tasks {
|
|
41
|
+
title := t.Title
|
|
42
|
+
if title == "" {
|
|
43
|
+
title = "(no title)"
|
|
44
|
+
}
|
|
45
|
+
fmt.Fprintf(&b, " %-52s %s\n", t.ID, title)
|
|
46
|
+
}
|
|
47
|
+
fmt.Fprintln(&b)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return b.String()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// hasAuditProposals reports whether any audit file under root contains a Proposed Changes section.
|
|
54
|
+
func hasAuditProposals(root string) bool {
|
|
55
|
+
releasesDir := filepath.Join(root, "releases")
|
|
56
|
+
releases, err := os.ReadDir(releasesDir)
|
|
57
|
+
if err != nil {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
for _, r := range releases {
|
|
61
|
+
if !r.IsDir() {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
epicsDir := filepath.Join(releasesDir, r.Name(), "epics")
|
|
65
|
+
epics, err := os.ReadDir(epicsDir)
|
|
66
|
+
if err != nil {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
for _, e := range epics {
|
|
70
|
+
if !e.IsDir() {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
short := e.Name()
|
|
74
|
+
if idx := strings.Index(short, "-"); idx >= 0 {
|
|
75
|
+
short = short[:idx]
|
|
76
|
+
}
|
|
77
|
+
auditPath := filepath.Join(epicsDir, e.Name(), short+"-Audit.md")
|
|
78
|
+
raw, err := os.ReadFile(auditPath)
|
|
79
|
+
if err != nil {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if strings.Contains(string(raw), "## Proposed Changes") {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false
|
|
88
|
+
}
|