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 干净
144 lines
4.6 KiB
Go
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
|
|
}
|