From 2a2e98bd8e5587fb7db476b3adf583a65fcbb96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=9A=E7=AB=A0=E6=B5=A9?= Date: Wed, 4 Nov 2026 15:57:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(version):=20=E5=85=BC=E5=AE=B9=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E5=BF=AB=E7=85=A7=E7=89=88=E6=9C=AC=E6=A0=87=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 识别“## 开发快照 (说明)”并保留旧 snapshot/v 格式。 版本号精确匹配、不跨二级标题、空正文不返回,archive 精确 SHA 绑定不变。 --- CHANGELOG.md | 10 ++ internal/governance/development_notes.go | 33 ++++- .../development_notes_plain_test.go | 133 ++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 internal/versionhistory/development_notes_plain_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c03b72..f807c79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 开发快照 0.1.352(版本说明标题格式兼容) + +### Fixed + +- **版本页可展开开发说明**:识别 `## 开发快照 (说明)` 标题,与旧 + `## 开发快照 snapshot/v - ...` 并存;版本号精确匹配,`0.1.35` 不会命中 + `0.1.351`,只读取到下一个二级标题,空正文不返回。 +- 历史提交仍按 commit-specific CHANGELOG、exact-SHA archive、annotated snapshot tag + 顺序取说明;archive 的 `目标提交` 绑定与正式版基线不变,未补写历史说明。 + ## 开发快照 0.1.351(索引预检使用目标映射 ID) ### Fixed diff --git a/internal/governance/development_notes.go b/internal/governance/development_notes.go index 9431f0f..7047296 100644 --- a/internal/governance/development_notes.go +++ b/internal/governance/development_notes.go @@ -16,9 +16,8 @@ func developmentNotes(changelog, version, sha string) string { return "" } lines := strings.Split(changelog, "\n") - prefix := "## 开发快照 snapshot/v" + version + " - " for i, line := range lines { - if strings.HasPrefix(line, prefix) { + if developmentHeadingMatches(line, version) { end := len(lines) for j := i + 1; j < len(lines); j++ { if strings.HasPrefix(lines[j], "## ") { @@ -39,3 +38,33 @@ func developmentNotes(changelog, version, sha string) string { } return "" } + +// developmentHeadingMatches 接受两种真实标题: +// - 旧格式:## 开发快照 snapshot/v0.1.1 - 2026-10-18 +// - 开发快照:## 开发快照 0.1.351(索引预检使用目标映射 ID) +// +// 版本号后必须是字符串结束或分隔符,避免 0.1.35 命中 0.1.351; +// 只读取到下一个二级标题,不跨版本吞正文。 +func developmentHeadingMatches(line, version string) bool { + prefixes := []string{ + "## 开发快照 snapshot/v" + version, + "## 开发快照 " + version, + } + for _, prefix := range prefixes { + if !strings.HasPrefix(line, prefix) { + continue + } + rest := line[len(prefix):] + if rest == "" { + return true + } + if strings.HasPrefix(rest, "—") || strings.HasPrefix(rest, "(") { + return true + } + switch rest[0] { + case ' ', '\t', '-', '(': + return true + } + } + return false +} diff --git a/internal/versionhistory/development_notes_plain_test.go b/internal/versionhistory/development_notes_plain_test.go new file mode 100644 index 0000000..c91a7f2 --- /dev/null +++ b/internal/versionhistory/development_notes_plain_test.go @@ -0,0 +1,133 @@ +package versionhistory + +import ( + "os" + "strings" + "testing" + "time" + + "knowledge-governance/internal/governance" +) + +const plainNotesFixture = `# Changelog + +## 开发快照 0.1.351(索引预检使用目标映射 ID) + +### Fixed + +- 索引预检使用目标映射 ID + +## 开发快照 0.1.35(前缀相似版本) + +### Fixed + +- 不应被 0.1.351 命中 + +## 开发快照 0.1.350(首次复用取消本库历史来源门槛) + +### Changed + +- 首次复用说明 +` + +func TestPlainDevelopmentHeadingExactVersion(t *testing.T) { + got := governance.ExtractDevelopmentNotes(plainNotesFixture, "0.1.351") + if got == "" || !strings.Contains(got, "索引预检使用目标映射 ID") || strings.Contains(got, "首次复用说明") { + t.Fatalf("plain 0.1.351 notes wrong: %q", got) + } + if other := governance.ExtractDevelopmentNotes(plainNotesFixture, "0.1.35"); strings.Contains(other, "索引预检") { + t.Fatalf("0.1.35 must not match 0.1.351 body: %q", other) + } + if other := governance.ExtractDevelopmentNotes(plainNotesFixture, "0.1.3"); other != "" { + t.Fatalf("0.1.3 must not prefix-match 0.1.35/0.1.351: %q", other) + } + if next := governance.ExtractDevelopmentNotes(plainNotesFixture, "0.1.350"); !strings.Contains(next, "首次复用说明") || strings.Contains(next, "索引预检") { + t.Fatalf("adjacent version bleed: %q", next) + } +} + +func TestPlainDevelopmentHeadingEmptyBodyAndLegacyFormat(t *testing.T) { + if got := governance.ExtractDevelopmentNotes("## 开发快照 0.1.351(空)\n\n## 开发快照 0.1.350(下一版)\n\n- x\n", "0.1.351"); got != "" { + t.Fatalf("empty body must stay empty: %q", got) + } + legacy := "## 开发快照 snapshot/v0.1.1 - 2026-10-18\n\n### Fixed\n\n- legacy body\n" + if got := governance.ExtractDevelopmentNotes(legacy, "0.1.1"); !strings.Contains(got, "legacy body") { + t.Fatalf("legacy snapshot format regressed: %q", got) + } +} + +func TestRealChangelogDevelopmentNotes(t *testing.T) { + body, err := os.ReadFile("../../CHANGELOG.md") + if err != nil { + t.Fatal(err) + } + changelog := string(body) + for _, tc := range []struct{ version, want string }{ + {"0.1.351", "索引预检"}, + {"0.1.350", "首次同步可复用"}, + } { + notes := governance.ExtractDevelopmentNotes(changelog, tc.version) + if notes == "" || !strings.Contains(notes, tc.want) { + t.Fatalf("real CHANGELOG %s notes missing: %q", tc.version, notes) + } + if governance.ExtractDevelopmentNotes(changelog, "0.1.35") != "" { + t.Fatal("prefix version matched a longer version") + } + } +} + +func TestDeriveVersionsPlainHeadingReleaseNotes(t *testing.T) { + text := "# Changelog\n\n## 开发快照 0.1.1(索引预检使用目标映射 ID)\n\n### Fixed\n\n- plain delivered notes\n\n## 开发快照 snapshot/v0.1.1 - 2026-10-18\n\n### Fixed\n\n- legacy duplicate heading\n\n## [0.1.0] - 2026-10-13\n\n### Added\n\n- baseline\n" + result := DeriveVersions(DeriveInput{ + Commits: []RawCommit{{SHA: "a", CommittedAt: time.Now()}, {SHA: "b", Parents: []string{"a"}, CommittedAt: time.Now()}}, + Tags: []RawTag{{Name: "v0.1.0", TargetSHA: "a", Annotated: true}}, + Changelog: func(string) (string, error) { + return text, nil + }, + }) + for _, record := range result.Records { + if record.CommitSHA == "b" { + if record.RecordKind != governance.RecordKindDevelopment || !strings.Contains(record.ReleaseNotes, "plain delivered notes") || + strings.Contains(record.ReleaseNotes, "legacy duplicate") { + t.Fatalf("DeriveVersions plain heading notes wrong: %q kind=%s label=%s", record.ReleaseNotes, record.RecordKind, record.VersionLabel) + } + return + } + } + t.Fatal("development record b missing") +} + +func TestPlainDevelopmentArchiveBindsExactCommit(t *testing.T) { + sha := strings.Repeat("a", 40) + archive := "## 开发快照 0.1.351(索引预检使用目标映射 ID)\n\n目标提交:" + sha + "\n\n### Fixed\n\n- archived plain notes\n" + if got := governance.ExtractDevelopmentNotesForCommit(archive, "0.1.351", sha); !strings.Contains(got, "archived plain notes") { + t.Fatalf("plain archive notes missing for exact commit: %q", got) + } + if got := governance.ExtractDevelopmentNotesForCommit(archive, "0.1.351", strings.Repeat("b", 40)); got != "" { + t.Fatalf("plain archive notes leaked to wrong commit: %q", got) + } +} + +func TestRealChangelogVersionInventory(t *testing.T) { + body, err := os.ReadFile("../../CHANGELOG.md") + if err != nil { + t.Fatal(err) + } + changelog := string(body) + for _, version := range []string{"0.1.351", "0.1.350", "0.1.349", "0.1.348"} { + notes := governance.ExtractDevelopmentNotes(changelog, version) + heading := "" + for _, line := range strings.Split(changelog, "\n") { + if strings.HasPrefix(line, "## 开发快照 "+version) || strings.HasPrefix(line, "## 开发快照 snapshot/v"+version) { + heading = line + break + } + } + t.Logf("version=%s heading=%q notes_bytes=%d", version, heading, len(notes)) + if version == "0.1.351" || version == "0.1.350" { + if notes == "" { + t.Fatalf("%s has a CHANGELOG section but no extracted notes", version) + } + } + } +}