fix(version): 兼容开发快照版本标题
build / build (push) Successful in 1m41s

识别“## 开发快照 <version>(说明)”并保留旧 snapshot/v 格式。
版本号精确匹配、不跨二级标题、空正文不返回,archive 精确 SHA 绑定不变。
This commit is contained in:
姚章浩
2026-11-04 15:57:53 +08:00
parent 92684c6f60
commit 2a2e98bd8e
3 changed files with 174 additions and 2 deletions
+10
View File
@@ -1,5 +1,15 @@
# Changelog
## 开发快照 0.1.352(版本说明标题格式兼容)
### Fixed
- **版本页可展开开发说明**:识别 `## 开发快照 <version>(说明)` 标题,与旧
`## 开发快照 snapshot/v<version> - ...` 并存;版本号精确匹配,`0.1.35` 不会命中
`0.1.351`,只读取到下一个二级标题,空正文不返回。
- 历史提交仍按 commit-specific CHANGELOG、exact-SHA archive、annotated snapshot tag
顺序取说明;archive 的 `目标提交` 绑定与正式版基线不变,未补写历史说明。
## 开发快照 0.1.351(索引预检使用目标映射 ID)
### Fixed
+31 -2
View File
@@ -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
}
@@ -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)
}
}
}
}