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
|
@@ -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
|
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package data
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
func FuzzExtractFrontmatter(f *testing.F) {
|
|
8
|
+
seeds := []string{
|
|
9
|
+
"---\nid: E01/T001\nstatus: planned\n---\nbody",
|
|
10
|
+
"---\n---\n",
|
|
11
|
+
"---\n\n---\n",
|
|
12
|
+
"---\nid: test\n---",
|
|
13
|
+
"",
|
|
14
|
+
"# no frontmatter",
|
|
15
|
+
"---\nid: [broken\n---\n",
|
|
16
|
+
"---\nname: héllo wörld\n---\n",
|
|
17
|
+
"---\nid: test\nstatus: in_progress\nphase: build\n---\nbody content",
|
|
18
|
+
"---\r\nid: test\r\n---\r\nbody",
|
|
19
|
+
}
|
|
20
|
+
for _, s := range seeds {
|
|
21
|
+
f.Add(s)
|
|
22
|
+
}
|
|
23
|
+
f.Fuzz(func(t *testing.T, content string) {
|
|
24
|
+
_, _ = extractFrontmatter(content)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func FuzzParseFrontmatter(f *testing.F) {
|
|
29
|
+
seeds := []string{
|
|
30
|
+
"---\nid: E01/T001\nstatus: planned\n---\nbody",
|
|
31
|
+
"---\n---\n",
|
|
32
|
+
"---\nid: [broken\n---\n",
|
|
33
|
+
"---\nname: héllo\n---\n",
|
|
34
|
+
"",
|
|
35
|
+
"no frontmatter",
|
|
36
|
+
"---\ntags: [a, b, c]\n---\n",
|
|
37
|
+
"---\nnested:\n key: val\n---\n",
|
|
38
|
+
}
|
|
39
|
+
for _, s := range seeds {
|
|
40
|
+
f.Add(s)
|
|
41
|
+
}
|
|
42
|
+
f.Fuzz(func(t *testing.T, content string) {
|
|
43
|
+
p := NewParser()
|
|
44
|
+
_, _ = p.ParseFrontmatter(content)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func FuzzSplitFrontmatterBody(f *testing.F) {
|
|
49
|
+
seeds := []string{
|
|
50
|
+
"---\nid: E01/T001\nstatus: planned\n---\nbody",
|
|
51
|
+
"---\n---\n",
|
|
52
|
+
"---\nkey: value\n---",
|
|
53
|
+
"",
|
|
54
|
+
"# no frontmatter",
|
|
55
|
+
"---\nid: test\nstatus: in_progress\n---\n\n## Section\n\nContent.",
|
|
56
|
+
"---\nid: test\n---\n\nbody with unicode: 日本語",
|
|
57
|
+
}
|
|
58
|
+
for _, s := range seeds {
|
|
59
|
+
f.Add(s)
|
|
60
|
+
}
|
|
61
|
+
f.Fuzz(func(t *testing.T, content string) {
|
|
62
|
+
yamlStr, body, err := SplitFrontmatterBody(content)
|
|
63
|
+
if err != nil {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
reconstructed := "---\n" + yamlStr + "\n---" + body
|
|
67
|
+
_, body2, err2 := SplitFrontmatterBody(reconstructed)
|
|
68
|
+
if err2 != nil {
|
|
69
|
+
t.Errorf("round-trip SplitFrontmatterBody failed on reconstructed: %v", err2)
|
|
70
|
+
}
|
|
71
|
+
if body2 != body {
|
|
72
|
+
t.Errorf("round-trip body mismatch: got %q, want %q", body2, body)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
@@ -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,14 @@ type taskFrontmatter struct {
|
|
|
84
88
|
Progress Progress `yaml:"progress"`
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
// normalizeLineEndings replaces Windows (CRLF) and legacy Mac (CR) line endings with LF.
|
|
92
|
+
func normalizeLineEndings(s string) string {
|
|
93
|
+
s = strings.ReplaceAll(s, "\r\n", "\n")
|
|
94
|
+
return strings.ReplaceAll(s, "\r", "\n")
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
func extractFrontmatter(content string) (string, error) {
|
|
88
|
-
normalized :=
|
|
98
|
+
normalized := normalizeLineEndings(content)
|
|
89
99
|
if !strings.HasPrefix(normalized, "---\n") {
|
|
90
100
|
return "", ErrNoFrontmatter
|
|
91
101
|
}
|
|
@@ -139,9 +149,15 @@ const legacyTodoColumn ColumnType = "todo"
|
|
|
139
149
|
|
|
140
150
|
func validateParsedTaskLifecycle(rawColumn ColumnType, task Task) error {
|
|
141
151
|
if rawColumn != "" && rawColumn != legacyTodoColumn && !IsCanonicalColumn(rawColumn) {
|
|
142
|
-
return fmt.Errorf("invalid task status %q: use planned, in_progress, or done", rawColumn)
|
|
152
|
+
return fmt.Errorf("invalid task status %q: use planned, in_progress, or done. Add 'status: planned' or 'status: in_progress' to task frontmatter", rawColumn)
|
|
143
153
|
}
|
|
144
|
-
|
|
154
|
+
if task.Column == ColumnInProgress && !IsCanonicalStage(task.Stage) && task.Stage != "" {
|
|
155
|
+
return fmt.Errorf("invalid phase %q: use build, test, or audit. Add 'phase: build' to task frontmatter", task.Stage)
|
|
156
|
+
}
|
|
157
|
+
if task.Column != ColumnInProgress && task.Stage != "" {
|
|
158
|
+
return nil
|
|
159
|
+
}
|
|
160
|
+
return nil
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
func firstStage(values ...ProgressStage) ProgressStage {
|
|
@@ -163,7 +179,7 @@ func firstList(values ...[]string) []string {
|
|
|
163
179
|
}
|
|
164
180
|
|
|
165
181
|
func extractChecklistItems(content, heading string) []CheckItem {
|
|
166
|
-
normalized :=
|
|
182
|
+
normalized := normalizeLineEndings(content)
|
|
167
183
|
start := strings.Index(normalized, heading)
|
|
168
184
|
if start == -1 {
|
|
169
185
|
return nil
|
|
@@ -201,7 +217,7 @@ func extractChecklistItems(content, heading string) []CheckItem {
|
|
|
201
217
|
}
|
|
202
218
|
|
|
203
219
|
func extractChecklistSection(content, heading string) []string {
|
|
204
|
-
normalized :=
|
|
220
|
+
normalized := normalizeLineEndings(content)
|
|
205
221
|
start := strings.Index(normalized, heading)
|
|
206
222
|
if start == -1 {
|
|
207
223
|
return nil
|
|
@@ -146,7 +146,7 @@ objective: "Style the board"
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
func
|
|
149
|
+
func TestParseTaskFile_allowsPhaseOutsideInProgress(t *testing.T) {
|
|
150
150
|
p := NewParser()
|
|
151
151
|
content := `---
|
|
152
152
|
id: E06/T001
|
|
@@ -158,12 +158,12 @@ objective: "Style the board"
|
|
|
158
158
|
# Task`
|
|
159
159
|
|
|
160
160
|
_, err := p.ParseTaskFile("test.md", content)
|
|
161
|
-
if err
|
|
162
|
-
t.
|
|
161
|
+
if err != nil {
|
|
162
|
+
t.Fatalf("ParseTaskFile() error = %v, want no error for legacy phase field", err)
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
func
|
|
166
|
+
func TestParseTaskFile_includesDefaultBuildForInProgress(t *testing.T) {
|
|
167
167
|
p := NewParser()
|
|
168
168
|
content := `---
|
|
169
169
|
id: E06/T001
|
|
@@ -173,9 +173,33 @@ objective: "Style the board"
|
|
|
173
173
|
|
|
174
174
|
# Task`
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
if err
|
|
178
|
-
t.
|
|
176
|
+
task, err := p.ParseTaskFile("test.md", content)
|
|
177
|
+
if err != nil {
|
|
178
|
+
t.Fatalf("ParseTaskFile() error = %v", err)
|
|
179
|
+
}
|
|
180
|
+
if task.Stage != StageBuild {
|
|
181
|
+
t.Fatalf("ParseTaskFile() expected StageBuild default, got %q", task.Stage)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func TestParseTaskFile_prefersPhaseOverLegacyStage(t *testing.T) {
|
|
186
|
+
p := NewParser()
|
|
187
|
+
content := `---
|
|
188
|
+
id: E06/T001
|
|
189
|
+
status: in_progress
|
|
190
|
+
stage: build
|
|
191
|
+
phase: test
|
|
192
|
+
objective: "Style the board"
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
# Task`
|
|
196
|
+
|
|
197
|
+
task, err := p.ParseTaskFile("test.md", content)
|
|
198
|
+
if err != nil {
|
|
199
|
+
t.Fatalf("ParseTaskFile() error = %v", err)
|
|
200
|
+
}
|
|
201
|
+
if task.Stage != StageTest {
|
|
202
|
+
t.Fatalf("Task.Stage = %q, want test from phase", task.Stage)
|
|
179
203
|
}
|
|
180
204
|
}
|
|
181
205
|
|
package/internal/data/task.go
CHANGED
|
@@ -26,15 +26,6 @@ const (
|
|
|
26
26
|
StageAudit ProgressStage = "audit"
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
type TaskStatus string
|
|
30
|
-
|
|
31
|
-
const (
|
|
32
|
-
StatusPlanned TaskStatus = "planned"
|
|
33
|
-
StatusInProgress TaskStatus = "in_progress"
|
|
34
|
-
StatusDone TaskStatus = "done"
|
|
35
|
-
StatusAudited TaskStatus = "audited"
|
|
36
|
-
)
|
|
37
|
-
|
|
38
29
|
type Progress struct {
|
|
39
30
|
Stage ProgressStage `yaml:"stage"`
|
|
40
31
|
Started bool `yaml:"started"`
|
package/internal/data/write.go
CHANGED
|
@@ -11,9 +11,91 @@ import (
|
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
var ErrMtimeConflict = fmt.Errorf("file modified since last read")
|
|
14
|
+
var ErrProposalNotFound = fmt.Errorf("target text not found in file")
|
|
15
|
+
|
|
16
|
+
// ApplyProposal replaces the first occurrence of old with newText in the file at path.
|
|
17
|
+
func ApplyProposal(path, old, newText string) error {
|
|
18
|
+
content, err := os.ReadFile(path)
|
|
19
|
+
if err != nil {
|
|
20
|
+
return fmt.Errorf("read %s: %w", path, err)
|
|
21
|
+
}
|
|
22
|
+
normalized := normalizeLineEndings(string(content))
|
|
23
|
+
if !strings.Contains(normalized, old) {
|
|
24
|
+
return fmt.Errorf("%w: %s", ErrProposalNotFound, path)
|
|
25
|
+
}
|
|
26
|
+
updated := strings.Replace(normalized, old, newText, 1)
|
|
27
|
+
return os.WriteFile(path, []byte(updated), 0644)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// UpdateEpicStatus sets the status field in the frontmatter of an E##-Detail.md file.
|
|
31
|
+
func UpdateEpicStatus(path, status string) error {
|
|
32
|
+
return updateFrontmatterField(path, "status", status)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// UpdateLastAudited sets the last_audited field in the frontmatter of Design.md.
|
|
36
|
+
func UpdateLastAudited(path, value string) error {
|
|
37
|
+
return updateFrontmatterField(path, "last_audited", value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// SplitFrontmatterBody splits content into frontmatter YAML and body.
|
|
41
|
+
func SplitFrontmatterBody(content string) (yamlStr string, body string, err error) {
|
|
42
|
+
normalized := normalizeLineEndings(content)
|
|
43
|
+
if !strings.HasPrefix(normalized, "---\n") {
|
|
44
|
+
return "", "", ErrNoFrontmatter
|
|
45
|
+
}
|
|
46
|
+
end := strings.Index(normalized[4:], "\n---")
|
|
47
|
+
if end == -1 {
|
|
48
|
+
return "", "", ErrNoClosingFrontmatter
|
|
49
|
+
}
|
|
50
|
+
yamlStr = strings.TrimSpace(normalized[4 : 4+end])
|
|
51
|
+
bodyStart := 4 + end + 4 // "---\n" + yaml + "\n---"
|
|
52
|
+
body = ""
|
|
53
|
+
if bodyStart < len(normalized) {
|
|
54
|
+
body = normalized[bodyStart:]
|
|
55
|
+
}
|
|
56
|
+
return yamlStr, body, nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func updateFrontmatterField(path, key, value string) error {
|
|
60
|
+
content, err := os.ReadFile(path)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return fmt.Errorf("read %s: %w", path, err)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
normalized := normalizeLineEndings(string(content))
|
|
66
|
+
|
|
67
|
+
raw, body, err := SplitFrontmatterBody(normalized)
|
|
68
|
+
if err != nil {
|
|
69
|
+
return fmt.Errorf("extract frontmatter: %w", err)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var doc yaml.Node
|
|
73
|
+
if err := yaml.Unmarshal([]byte(raw), &doc); err != nil {
|
|
74
|
+
return fmt.Errorf("parse yaml: %w", err)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if doc.Kind != yaml.DocumentNode || len(doc.Content) == 0 {
|
|
78
|
+
return fmt.Errorf("unexpected yaml structure")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
mapping := doc.Content[0]
|
|
82
|
+
if mapping.Kind != yaml.MappingNode {
|
|
83
|
+
return fmt.Errorf("frontmatter is not a mapping")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setMappingField(mapping, key, value)
|
|
87
|
+
|
|
88
|
+
out, err := yaml.Marshal(&doc)
|
|
89
|
+
if err != nil {
|
|
90
|
+
return fmt.Errorf("marshal yaml: %w", err)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
newContent := "---\n" + strings.TrimSpace(string(out)) + "\n---" + body
|
|
94
|
+
return os.WriteFile(path, []byte(newContent), 0644)
|
|
95
|
+
}
|
|
14
96
|
|
|
15
97
|
func WriteTaskStatus(path string, task *Task, expectedMtime time.Time) error {
|
|
16
|
-
if err := ValidateTaskLifecycle(
|
|
98
|
+
if err := ValidateTaskLifecycle(task); err != nil {
|
|
17
99
|
return err
|
|
18
100
|
}
|
|
19
101
|
|
|
@@ -31,9 +113,9 @@ func WriteTaskStatus(path string, task *Task, expectedMtime time.Time) error {
|
|
|
31
113
|
return fmt.Errorf("read %s: %w", path, err)
|
|
32
114
|
}
|
|
33
115
|
|
|
34
|
-
normalized :=
|
|
116
|
+
normalized := normalizeLineEndings(string(content))
|
|
35
117
|
|
|
36
|
-
raw, err :=
|
|
118
|
+
raw, body, err := SplitFrontmatterBody(normalized)
|
|
37
119
|
if err != nil {
|
|
38
120
|
return fmt.Errorf("extract frontmatter: %w", err)
|
|
39
121
|
}
|
|
@@ -56,8 +138,10 @@ func WriteTaskStatus(path string, task *Task, expectedMtime time.Time) error {
|
|
|
56
138
|
|
|
57
139
|
if task.Stage == "" {
|
|
58
140
|
removeMappingField(mapping, "phase")
|
|
141
|
+
removeMappingField(mapping, "stage")
|
|
59
142
|
} else {
|
|
60
143
|
setMappingField(mapping, "phase", string(task.Stage))
|
|
144
|
+
removeMappingField(mapping, "stage")
|
|
61
145
|
}
|
|
62
146
|
|
|
63
147
|
out, err := yaml.Marshal(&doc)
|
|
@@ -65,13 +149,6 @@ func WriteTaskStatus(path string, task *Task, expectedMtime time.Time) error {
|
|
|
65
149
|
return fmt.Errorf("marshal yaml: %w", err)
|
|
66
150
|
}
|
|
67
151
|
|
|
68
|
-
delimLen := 4
|
|
69
|
-
bodyStart := delimLen + len(raw) + delimLen
|
|
70
|
-
body := ""
|
|
71
|
-
if bodyStart < len(normalized) {
|
|
72
|
-
body = normalized[bodyStart:]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
152
|
newContent := "---\n" + strings.TrimSpace(string(out)) + "\n---" + body
|
|
76
153
|
|
|
77
154
|
return os.WriteFile(path, []byte(newContent), 0644)
|
|
@@ -115,7 +192,7 @@ func WriteRouterState(root string, state *RouterState, expectedMtime time.Time)
|
|
|
115
192
|
return fmt.Errorf("read %s: %w", path, err)
|
|
116
193
|
}
|
|
117
194
|
|
|
118
|
-
normalized :=
|
|
195
|
+
normalized := normalizeLineEndings(string(content))
|
|
119
196
|
|
|
120
197
|
startIdx := strings.Index(normalized, stateBlockStart)
|
|
121
198
|
if startIdx == -1 {
|