Files
knowledge-governance/internal/workflows/testweknora/validate.go
T
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(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 干净
2026-09-30 10:54:06 +08:00

144 lines
4.6 KiB
Go

package testweknora
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"time"
"knowledge-governance/internal/governance"
"knowledge-governance/internal/jobs"
postgresstore "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/platform/weknora"
)
var (
ErrInvalidValidateJobPayload = errors.New("test_validate_invalid_payload")
ErrValidateStateConflict = errors.New("test_validate_state_conflict")
ErrValidateRecordFailed = errors.New("test_validate_record_failed")
)
type ValidateStore interface {
LoadPublicationForValidation(context.Context, string) (postgresstore.PublicationForValidation, error)
AppendAcceptanceResult(context.Context, governance.AcceptanceResult) error
}
type ValidateClient interface {
GetKnowledge(context.Context, string) (weknora.Knowledge, error)
ListChunks(context.Context, string, int, int, []string) ([]weknora.Chunk, int64, error)
}
type ValidateHandlerConfig struct {
Store ValidateStore
Client ValidateClient
RunID string
PolicyVersion string
Now func() time.Time
}
type ValidateHandler struct {
store ValidateStore
client ValidateClient
runID string
policyVersion string
now func() time.Time
}
type validatePayload struct {
PublicationID string `json:"publication_id"`
}
func NewValidateHandler(config ValidateHandlerConfig) (*ValidateHandler, error) {
if config.Store == nil || config.Client == nil || config.PolicyVersion == "" {
return nil, errors.New("invalid test validate handler configuration")
}
if config.RunID == "" {
config.RunID = "default"
}
if config.Now == nil {
config.Now = time.Now
}
return &ValidateHandler{
store: config.Store, client: config.Client, runID: config.RunID,
policyVersion: config.PolicyVersion, now: config.Now,
}, nil
}
func (handler *ValidateHandler) Handle(ctx context.Context, job jobs.Job) error {
var payload validatePayload
if err := decodeStrictJSON(job.Payload, &payload); err != nil || payload.PublicationID == "" {
return ErrInvalidValidateJobPayload
}
publication, err := handler.store.LoadPublicationForValidation(ctx, payload.PublicationID)
if err != nil {
return ErrValidateStateConflict
}
now := handler.now()
runID := handler.runID
if job.ID != "" {
runID = job.ID
}
knowledge, knowledgeErr := handler.client.GetKnowledge(ctx, publication.KnowledgeID)
knowledgeState := governance.AcceptancePassed
knowledgeEvidence := json.RawMessage(`{"matched":true}`)
if knowledgeErr != nil || knowledge.ID != publication.KnowledgeID || knowledge.KnowledgeBaseID != publication.TestKBID {
knowledgeState = governance.AcceptanceFailed
knowledgeEvidence = json.RawMessage(`{"matched":false}`)
}
if err := handler.append(ctx, publication.ID, runID, "knowledge_exists", knowledgeState, knowledgeEvidence, now); err != nil {
return err
}
_, chunkTotal, chunksErr := handler.client.ListChunks(ctx, publication.KnowledgeID, 1, 1, nil)
chunksState := governance.AcceptancePassed
if chunksErr != nil || chunkTotal <= 0 {
chunksState = governance.AcceptanceFailed
}
if err := handler.append(ctx, publication.ID, runID, "chunks_present", chunksState, mustJSON(map[string]any{"chunk_total": chunkTotal}), now); err != nil {
return err
}
if err := handler.append(ctx, publication.ID, runID, "retrieval_hit", governance.AcceptanceManualRequired, json.RawMessage(`{"reason":"thresholds_unresolved"}`), now); err != nil {
return err
}
return nil
}
func (handler *ValidateHandler) append(ctx context.Context, publicationID, runID, checkType string, state governance.AcceptanceState, evidence json.RawMessage, observedAt time.Time) error {
result := governance.AcceptanceResult{
ID: acceptanceResultID(publicationID, runID, checkType),
PublicationID: publicationID,
RunID: runID,
CheckType: checkType,
State: state,
PolicyVersion: handler.policyVersion,
EvidenceSummary: evidence,
ObservedAt: observedAt,
}
if err := handler.store.AppendAcceptanceResult(ctx, result); err != nil {
return ErrValidateRecordFailed
}
return nil
}
func acceptanceResultID(publicationID, runID, checkType string) string {
digest := sha256.New()
_, _ = digest.Write([]byte(publicationID))
_, _ = digest.Write([]byte{0})
_, _ = digest.Write([]byte(runID))
_, _ = digest.Write([]byte{0})
_, _ = digest.Write([]byte(checkType))
return "acc-" + hex.EncodeToString(digest.Sum(nil))[:32]
}
func mustJSON(value any) json.RawMessage {
data, err := json.Marshal(value)
if err != nil {
return json.RawMessage(`{}`)
}
return data
}