// 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 }