Files
姚章浩 e313ae12f2
build / build (push) Successful in 1m18s
feat(library): 真实差异比对 + 孤儿作业回收 + 验收矩阵与台账
为什么:发布管理的差异一直显示"未接入",业务作业也可能因队列丢作业而永久占锁;
本轮把这两件做成真实实现,并把 30 个验收项与 CI/版本身份落成可审计文档。

- internal/librarydiff:稳定文档 ID + 排除易变列的整行摘要做增改撤销;overview 在
  生产权威版本与测试封存都在时给出 computed 差异并标注两侧快照,缺一侧仍是 unknown。
- 发布管理"本次变更"列文档身份与比对版本;无差异时明确写"没有文档差异"。
- ReconcileLibraryJobs:worker 启动清理队列已无对应作业的孤儿,避免永久互斥。
- docs/library-acceptance-matrix.md 与 docs/release-ledger.md 固定证据与版本事实。
2026-10-28 14:39:17 +08:00

125 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package librarydiff 比较两个已归档整库版本的知识文档集合。
//
// 身份用稳定文档 ID(不是标题、不是顺序),内容用排除易变字段后的整行摘要:
// 只有真实内容/元数据变化才算"更新",updated_at 之类的噪声不会引发假差异。
// 撤销(生产有、目标没有)单列,不能当作"无变化"。
package librarydiff
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"sort"
)
// volatileFields 是每次保存都会变、但不代表知识内容变化的列。
var volatileFields = []string{"updated_at", "updated_by", "last_edited_at"}
type Document struct {
ID string `json:"id"`
Title string `json:"title"`
Digest string `json:"digest"`
}
type Result struct {
Added []Document `json:"added"`
Updated []Document `json:"updated"`
Removed []Document `json:"removed"`
// BaseCount/TargetCount 是两侧文档数,便于核对"差异之和是否自洽"。
BaseCount int `json:"base_count"`
TargetCount int `json:"target_count"`
}
// ReadDocuments 读取归档里 knowledges 部件的行数组。
func ReadDocuments(r io.Reader) ([]map[string]any, error) {
decoder := json.NewDecoder(r)
token, err := decoder.Token()
if err != nil || token != json.Delim('[') {
return nil, errors.New("knowledge component must be a JSON row array")
}
rows := make([]map[string]any, 0, 64)
for decoder.More() {
var row map[string]any
if err = decoder.Decode(&row); err != nil {
return nil, fmt.Errorf("decode knowledge row: %w", err)
}
rows = append(rows, row)
}
if _, err = decoder.Token(); err != nil {
return nil, fmt.Errorf("close knowledge array: %w", err)
}
return rows, nil
}
func documentOf(row map[string]any) (Document, error) {
id, _ := row["id"].(string)
if id == "" {
return Document{}, errors.New("knowledge row without id")
}
canonical := make(map[string]any, len(row))
for key, value := range row {
canonical[key] = value
}
for _, key := range volatileFields {
delete(canonical, key)
}
encoded, err := json.Marshal(canonical)
if err != nil {
return Document{}, fmt.Errorf("encode knowledge row: %w", err)
}
sum := sha256.Sum256(encoded)
title, _ := row["title"].(string)
return Document{ID: id, Title: title, Digest: hex.EncodeToString(sum[:])}, nil
}
// Compare 以 base 为基准(通常是生产当前版本),target 为拟发布版本。
func Compare(base, target []map[string]any) (Result, error) {
baseDocs, err := index(base)
if err != nil {
return Result{}, err
}
targetDocs, err := index(target)
if err != nil {
return Result{}, err
}
result := Result{Added: []Document{}, Updated: []Document{}, Removed: []Document{},
BaseCount: len(baseDocs), TargetCount: len(targetDocs)}
for id, doc := range targetDocs {
previous, ok := baseDocs[id]
if !ok {
result.Added = append(result.Added, doc)
continue
}
if previous.Digest != doc.Digest {
result.Updated = append(result.Updated, doc)
}
}
for id, doc := range baseDocs {
if _, ok := targetDocs[id]; !ok {
result.Removed = append(result.Removed, doc)
}
}
sort.Slice(result.Added, func(i, j int) bool { return result.Added[i].ID < result.Added[j].ID })
sort.Slice(result.Updated, func(i, j int) bool { return result.Updated[i].ID < result.Updated[j].ID })
sort.Slice(result.Removed, func(i, j int) bool { return result.Removed[i].ID < result.Removed[j].ID })
return result, nil
}
func index(rows []map[string]any) (map[string]Document, error) {
docs := make(map[string]Document, len(rows))
for _, row := range rows {
doc, err := documentOf(row)
if err != nil {
return nil, err
}
if _, exists := docs[doc.ID]; exists {
return nil, fmt.Errorf("duplicate knowledge id %s", doc.ID)
}
docs[doc.ID] = doc
}
return docs, nil
}