Files
knowledge-governance/internal/governance/library_flow.go
T
姚章浩 517d5f862d feat(library): 首次发布接上目标权威基线与整库空判定
目标非空又没有生效版本时,首次发布以前只能停在"权威版本未知"。现在有了
真实入口:管理员发起 baseline 作业,worker 在维护隔离下把目标整库捕获成
归档快照,再用同一套 Verify 证明目标与快照逐类一致,最后登记
production_effective(证据写明 origin=target_baseline、快照 ID、逐表行数、
目标索引映射)。不写目标内容、不清空目标、不手写版本。

同时修两处会误导人的判定:
- productionScopeEmpty 原来只数 knowledges,"文档删了但分块/Wiki/资源还在"
  的目标会被当成空库;改用适配层同一份表清单与范围过滤逐表计数。
- 概览里的"测试封存候选"原来取该 scope 最新一份归档,而归档里还有生产
  预备份与目标基线快照;改为只认版本登记里 kind=test_sealed 的那一份。

迁移 075 放开 kind='baseline' 并把它并入 sync/restore 的写互斥索引;
worker 重启时用同一作业 id 接管未完成的基线窗口。维护接口补 capability,
页面才能区分"未运行维护"与"维护能力没接线"。

验证:go build ./... / go vet ./... 通过;新增用例覆盖管理员校验、已有生效
版本拒绝、目标读不到时零作业零版本、候选来自登记、缺项文案指向基线入口、
capability 逐项缺失;反向自检(把候选改回按最新归档取、把文案改回旧句、
去掉管理员判断)三项均按预期变红后复原。
2026-10-29 18:16:21 +08:00

194 lines
8.5 KiB
Go
Raw 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 governance
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"time"
)
// 整库版本与作业的领域类型:与队列(jobs)分开——队列负责租约与重试,
// 这里负责业务身份、阶段与结果,重启后仍能如实展示。
type LibraryVersionKind string
const (
// LibraryVersionTestSealed 是测试库封存产物:只证明一份完整测试版本被封存,
// 不代表生产生效,也不代表做过生产恢复演练。
LibraryVersionTestSealed LibraryVersionKind = "test_sealed"
// LibraryVersionProductionEffective 是生产当前权威版本:只有生产验证通过并
// 记录证据后才能写入,测试快照不能顶替它。
LibraryVersionProductionEffective LibraryVersionKind = "production_effective"
// LibraryVersionProductionPrebackup 是每次生产写入前的独立预备份。
LibraryVersionProductionPrebackup LibraryVersionKind = "production_prebackup"
)
type LibraryJobKind string
const (
LibraryJobSeal LibraryJobKind = "seal"
LibraryJobSync LibraryJobKind = "sync"
// LibraryJobRestore 是按指定整库版本恢复:与同步分开,授权与阶段也独立。
LibraryJobRestore LibraryJobKind = "restore"
// LibraryJobBaseline 建立首次发布目标的权威基线:在维护隔离下把目标整库
// 读出来封存成快照并逐类校验,再登记成 production_effective。
// 它不写目标内容、不清空目标、不猜版本——基线只能来自真实捕获与校验。
LibraryJobBaseline LibraryJobKind = "baseline"
)
type LibraryJobState string
const (
LibraryJobPending LibraryJobState = "pending"
LibraryJobRunning LibraryJobState = "running"
LibraryJobSucceeded LibraryJobState = "succeeded"
LibraryJobFailed LibraryJobState = "failed"
LibraryJobCancelled LibraryJobState = "cancelled"
)
// ErrLibraryJobActive:同一目标同一类作业已有在飞作业(数据库级互斥)。
var ErrLibraryJobActive = errors.New("library job already active for this target")
// ErrLibraryJobIdempotencyConflict:同一幂等键对应了不同的请求体。
var ErrLibraryJobIdempotencyConflict = errors.New("library job idempotency key reused with a different request")
type LibraryStage struct {
Stage string `json:"stage"`
State string `json:"state"`
// Total/Completed/BytesTotal/BytesDone 为 nil 表示该维度没有可靠数据,
// 界面必须显示"未知",不得当 0 或据此算百分比。
Total *int64 `json:"total"`
Completed *int64 `json:"completed"`
BytesTotal *int64 `json:"bytes_total"`
BytesDone *int64 `json:"bytes_done"`
Detail string `json:"detail"`
UpdatedAt time.Time `json:"updated_at"`
}
type LibraryJob struct {
ID string `json:"id"`
Scope string `json:"scope"`
Kind LibraryJobKind `json:"kind"`
State LibraryJobState `json:"state"`
Stage string `json:"stage"`
IdempotencyKey string `json:"-"`
RequestDigest string `json:"request_digest"`
QueueJobID string `json:"queue_job_id"`
SnapshotID string `json:"snapshot_id"`
TargetSnapshotID string `json:"target_snapshot_id"`
Actor string `json:"actor"`
Error string `json:"error"`
Recovery string `json:"recovery"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
FinishedAt *time.Time `json:"finished_at"`
Stages []LibraryStage `json:"stages"`
}
type LibraryVersion struct {
ID string `json:"id"`
Scope string `json:"scope"`
Kind LibraryVersionKind `json:"kind"`
SnapshotID string `json:"snapshot_id"`
VersionLabel string `json:"version_label"`
SourceJobID string `json:"source_job_id"`
Evidence map[string]any `json:"evidence"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
SupersededAt *time.Time `json:"superseded_at"`
}
// LibraryRequestDigest 绑定一次请求的确切含义:同键不同体必须被拒绝,而不是
// 复用旧作业。只覆盖会影响执行结果的字段;actor 不参与摘要。
// LibraryVersionID 生成版本记录 ID:library_versions.id 是 varchar(64),
// 用完整快照 ID 拼会超长(真实踩过),所以用稳定短摘要——同样的输入永远得到同一个 ID,
// 收尾重试因此是幂等的。
func LibraryVersionID(kind LibraryVersionKind, scope, snapshotID string) string {
sum := sha256.Sum256([]byte(string(kind) + "|" + scope + "|" + snapshotID))
return "libver-" + string(kind)[:0] + hex.EncodeToString(sum[:])[:16]
}
// QueueJobIDFor 生成队列作业 ID:jobs.id 是 varchar(36),
// 直接用业务作业 ID 加后缀会超长(真实踩过),所以用固定长度摘要。
func QueueJobIDFor(libraryJobID string) string {
sum := sha256.Sum256([]byte(libraryJobID))
return "q" + hex.EncodeToString(sum[:])[:32]
}
func LibraryRequestDigest(scope string, kind LibraryJobKind, payload map[string]any) (string, error) {
canonical := map[string]any{"scope": scope, "kind": string(kind), "payload": payload}
raw, err := json.Marshal(canonical)
if err != nil {
return "", err
}
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:]), nil
}
// SyncStages 是生产同步作业的固定阶段顺序:先备份并核验恢复条件,再写生产,
// 最后验证;失败时走恢复而不是把作业标成功。
func SyncStages() []string {
// finalize 是收尾:登记生产生效版本。登记失败不得报成功,所以它是独立阶段。
return []string{"prebackup", "database", "resources", "index", "wiki", "verify", "finalize", "rollback"}
}
// SealStages 是封存作业的阶段:导出是原子操作,没有分项进度。
func SealStages() []string {
return []string{"export"}
}
// BaselineStages 是"建立目标权威基线"的阶段:捕获目标整库 → 与快照逐类校验
// → 登记生效版本。没有 rollback 阶段:基线过程不写目标内容。
func BaselineStages() []string {
return []string{"capture", "verify", "finalize"}
}
// LibraryReleaseAuthorization 是生产发布授权:绑定目标、快照、参数摘要与有效期。
type LibraryReleaseAuthorization struct {
ID string `json:"id"`
Scope string `json:"scope"`
SnapshotID string `json:"snapshot_id"`
ParamsDigest string `json:"params_digest"`
AuthorizedBy string `json:"authorized_by"`
ExpiresAt time.Time `json:"expires_at"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// ValidAt 只在未撤销且未过期时为真;参数摘要由调用方显式比对。
func (a LibraryReleaseAuthorization) ValidAt(now time.Time) bool {
return a.RevokedAt == nil && now.Before(a.ExpiresAt)
}
// LibraryDeadLetterRelease 是治理平台对一条上游死信的精确解除记录。上游行保持原样,
// 这条记录只说明"这一行已被审计、重放并结清";指纹一变就不再命中。
type LibraryDeadLetterRelease struct {
ID string `json:"id"`
Instance string `json:"instance"`
TenantID uint64 `json:"tenant_id"`
KnowledgeBaseID string `json:"kb_id"`
DeadLetterID int64 `json:"dead_letter_id"`
Fingerprint string `json:"fingerprint"`
TaskType string `json:"task_type"`
Scope string `json:"scope"`
ScopeID string `json:"scope_id"`
RelatedID string `json:"related_id"`
ReplayQueue string `json:"replay_queue"`
ReplayTaskID string `json:"replay_task_id"`
ReplayedAt time.Time `json:"replayed_at"`
ReplayEvidence string `json:"replay_evidence"`
ObservationDigest string `json:"observation_digest"`
Observation json.RawMessage `json:"observation"`
Note string `json:"note"`
Actor string `json:"actor"`
CreatedAt time.Time `json:"created_at"`
}
// LibraryDeadLetterReleaseID 生成解除记录 ID。同一实例同一指纹永远得到同一个 ID,
// 重复提交返回已存记录而不是造第二条审计。
func LibraryDeadLetterReleaseID(instance, fingerprint string) string {
sum := sha256.Sum256([]byte(instance + "|" + fingerprint))
return "dldr-" + hex.EncodeToString(sum[:])[:24]
}