dcdc8d1ddd
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环): - 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口) - 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入 parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截 - 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过): * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id) * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配 * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数 * 回滚 job payload 支持 prior_execution_id(strict 解码) - 三通道发布+回滚演练实测:publish 4/4 verified,检索命中, rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false - 前端 A·Data-console 风格全站统一,120 用例通过 - 门禁:go vet / go test 全包通过,gofmt 干净
142 lines
4.3 KiB
Go
142 lines
4.3 KiB
Go
package pp
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
type LegacyStatus string
|
|
|
|
const (
|
|
StatusCompleted LegacyStatus = "completed"
|
|
StatusContentAlias LegacyStatus = "content_alias"
|
|
StatusOverLimit LegacyStatus = "over_limit"
|
|
)
|
|
|
|
type Item struct {
|
|
KBID string `json:"kb_id"`
|
|
SourceURLHash string `json:"source_url_hash"`
|
|
SourceIDs []string `json:"source_ids"`
|
|
Bytes int64 `json:"bytes"`
|
|
SHA256 string `json:"sha256"`
|
|
Status LegacyStatus `json:"status"`
|
|
KnowledgeID string `json:"knowledge_id"`
|
|
CanonicalSourceURLHash string `json:"canonical_source_url_hash"`
|
|
}
|
|
|
|
type Snapshot struct {
|
|
KBID string
|
|
EventCount int
|
|
UnresolvedCount int
|
|
Items []Item
|
|
}
|
|
|
|
func FoldLedger(reader io.Reader) (Snapshot, error) {
|
|
scanner := bufio.NewScanner(reader)
|
|
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
|
|
terminal := make(map[string]Item)
|
|
seen := make(map[string]struct{})
|
|
result := Snapshot{}
|
|
for scanner.Scan() {
|
|
result.EventCount++
|
|
var item Item
|
|
if err := json.Unmarshal(scanner.Bytes(), &item); err != nil {
|
|
return Snapshot{}, fmt.Errorf("decode ledger line %d: %w", result.EventCount, err)
|
|
}
|
|
if result.KBID == "" {
|
|
result.KBID = item.KBID
|
|
} else if item.KBID != result.KBID {
|
|
return Snapshot{}, fmt.Errorf("ledger contains multiple knowledge bases at line %d", result.EventCount)
|
|
}
|
|
if item.SourceURLHash != "" {
|
|
seen[item.SourceURLHash] = struct{}{}
|
|
}
|
|
if item.Status == StatusCompleted || (item.Status == StatusContentAlias && item.KnowledgeID != "") {
|
|
if err := validateTerminalItem(item); err != nil {
|
|
return Snapshot{}, fmt.Errorf("invalid terminal ledger record at line %d: %w", result.EventCount, err)
|
|
}
|
|
terminal[item.SourceURLHash] = item
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return Snapshot{}, fmt.Errorf("read ledger: %w", err)
|
|
}
|
|
result.Items = make([]Item, 0, len(terminal))
|
|
for _, item := range terminal {
|
|
result.Items = append(result.Items, item)
|
|
}
|
|
result.UnresolvedCount = len(seen) - len(terminal)
|
|
sort.Slice(result.Items, func(i, j int) bool {
|
|
return result.Items[i].SourceURLHash < result.Items[j].SourceURLHash
|
|
})
|
|
return result, nil
|
|
}
|
|
|
|
func validateTerminalItem(item Item) error {
|
|
if item.KBID == "" {
|
|
return fmt.Errorf("kb_id is required")
|
|
}
|
|
if !governance.ValidSHA256(item.SourceURLHash) {
|
|
return fmt.Errorf("source_url_hash must be a canonical hash")
|
|
}
|
|
if len(item.SourceIDs) == 0 {
|
|
return fmt.Errorf("source_ids are required")
|
|
}
|
|
for _, sourceID := range item.SourceIDs {
|
|
if sourceID == "" {
|
|
return fmt.Errorf("source_ids cannot contain an empty value")
|
|
}
|
|
}
|
|
if item.Bytes < 0 {
|
|
return fmt.Errorf("bytes cannot be negative")
|
|
}
|
|
if item.SHA256 != "" && !governance.ValidSHA256(item.SHA256) {
|
|
return fmt.Errorf("sha256 must be empty or a canonical hash")
|
|
}
|
|
if item.KnowledgeID == "" {
|
|
return fmt.Errorf("knowledge_id is required")
|
|
}
|
|
if item.Status == StatusContentAlias && !governance.ValidSHA256(item.CanonicalSourceURLHash) {
|
|
return fmt.Errorf("canonical_source_url_hash must be a canonical hash")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateSnapshot(snapshot Snapshot) error {
|
|
if snapshot.UnresolvedCount != 0 {
|
|
return fmt.Errorf("snapshot contains %d unresolved source keys", snapshot.UnresolvedCount)
|
|
}
|
|
bySourceHash := make(map[string]Item, len(snapshot.Items))
|
|
for _, item := range snapshot.Items {
|
|
if item.Status == StatusOverLimit {
|
|
if item.Bytes <= LegacyMaxBytes {
|
|
return fmt.Errorf("over-limit item does not exceed the legacy size limit")
|
|
}
|
|
bySourceHash[item.SourceURLHash] = item
|
|
continue
|
|
}
|
|
if !governance.ValidSHA256(item.SHA256) {
|
|
return fmt.Errorf("snapshot item is missing a verified SHA-256")
|
|
}
|
|
bySourceHash[item.SourceURLHash] = item
|
|
}
|
|
for _, item := range snapshot.Items {
|
|
if item.Status != StatusContentAlias {
|
|
continue
|
|
}
|
|
canonical, ok := bySourceHash[item.CanonicalSourceURLHash]
|
|
if !ok || canonical.Status != StatusCompleted {
|
|
return fmt.Errorf("alias does not reference a canonical completed item")
|
|
}
|
|
if item.SHA256 != canonical.SHA256 || item.KnowledgeID != canonical.KnowledgeID {
|
|
return fmt.Errorf("alias metadata does not match canonical completed item")
|
|
}
|
|
}
|
|
return nil
|
|
}
|