Files
knowledge-governance/internal/platform/postgres/release_plan.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

133 lines
6.0 KiB
Go

package postgres
import (
"context"
"errors"
"fmt"
"knowledge-governance/internal/governance"
)
const listReleasePlanSQL = `
WITH ` + releaseGateCTEs + `,
source_truth AS (
SELECT source.id AS source_item_id, source.title, source.revision, source.source_change,
COALESCE(item_rule.visibility, directory_rule.visibility, source.visibility) AS desired_policy
FROM source_items source
LEFT JOIN source_item_scope_overrides item_rule ON item_rule.source_item_id=source.id
LEFT JOIN LATERAL (
SELECT rule.visibility
FROM directory_scope_rules rule
WHERE rule.source_connection_id=source.source_connection_id
AND (rule.directory_path='' OR source.normalized_path=rule.directory_path OR source.normalized_path LIKE rule.directory_path||'/%')
ORDER BY LENGTH(rule.directory_path) DESC
LIMIT 1
) directory_rule ON TRUE
),
desired AS (
SELECT truth.source_item_id, truth.title, scope.target_scope
FROM source_truth truth
CROSS JOIN LATERAL unnest(
CASE
WHEN truth.source_change='deleted' OR truth.desired_policy='blocked' THEN ARRAY[]::TEXT[]
WHEN truth.desired_policy='both' THEN ARRAY['internal','external']::TEXT[]
ELSE ARRAY[truth.desired_policy]::TEXT[]
END
) scope(target_scope)
),
test_ready AS (
SELECT DISTINCT ON (version.source_item_id, publication.target_scope)
version.source_item_id, publication.target_scope, publication.id AS publication_id,
ready.review_id AS release_revision
FROM release_ready ready
JOIN test_publications publication ON publication.id=ready.publication_id
JOIN source_item_versions version ON version.id=publication.source_item_version_id
ORDER BY version.source_item_id, publication.target_scope, publication.updated_at DESC, publication.id DESC
),
latest_production AS (
SELECT DISTINCT ON (version.source_item_id, publication.target_scope)
version.source_item_id, publication.target_scope,
publication.id AS publication_id, item.production_revision
FROM release_item_executions item
JOIN release_executions execution ON execution.id=item.release_execution_id
JOIN test_publications publication ON publication.id=item.publication_id
JOIN source_item_versions version ON version.id=publication.source_item_version_id
WHERE execution.state IN ('completed','rolled_back')
AND item.state IN ('verified','rolled_back')
ORDER BY version.source_item_id, publication.target_scope, execution.completed_at DESC NULLS LAST, execution.id DESC
),
production AS (
SELECT * FROM latest_production WHERE production_revision<>'' AND production_revision<>'absent'
),
all_scopes AS (
SELECT source_item_id,target_scope FROM desired
UNION
SELECT source_item_id,target_scope FROM production
),
plan AS (
SELECT COALESCE(test.publication_id, production.publication_id, '') AS publication_id,
truth.title,
truth.source_change,
scope.target_scope,
CASE
WHEN truth.source_change='unavailable' THEN 'blocked'
WHEN desired.source_item_id IS NULL AND production.source_item_id IS NOT NULL THEN 'delete'
WHEN desired.source_item_id IS NOT NULL AND test.publication_id IS NULL THEN 'blocked'
WHEN production.source_item_id IS NULL THEN 'add'
WHEN production.production_revision<>test.release_revision THEN 'update'
ELSE 'unchanged'
END AS action,
CASE WHEN desired.source_item_id IS NULL THEN 'not_required' WHEN test.publication_id IS NULL THEN 'not_ready' ELSE 'ready' END AS test_state,
CASE WHEN production.source_item_id IS NULL THEN 'absent' ELSE 'present' END AS production_state
FROM all_scopes scope
JOIN source_truth truth ON truth.source_item_id=scope.source_item_id
LEFT JOIN desired ON desired.source_item_id=scope.source_item_id AND desired.target_scope=scope.target_scope
LEFT JOIN test_ready test ON test.source_item_id=scope.source_item_id AND test.target_scope=scope.target_scope
LEFT JOIN production ON production.source_item_id=scope.source_item_id AND production.target_scope=scope.target_scope
)
SELECT publication_id,title,target_scope,action,test_state,production_state,
action IN ('add', 'update', 'delete') AS can_release,
CASE WHEN action='blocked' AND source_change='unavailable' THEN 'source_unavailable'
WHEN action='blocked' THEN 'test_not_ready'
ELSE '' END AS blocker
FROM plan
ORDER BY CASE action WHEN 'delete' THEN 0 WHEN 'blocked' THEN 1 WHEN 'update' THEN 2 WHEN 'add' THEN 3 ELSE 4 END, title, target_scope
`
func (s *Store) ListReleasePlan(ctx context.Context) (governance.ReleasePlan, error) {
rows, err := s.db.Query(ctx, listReleasePlanSQL)
if err != nil {
return governance.ReleasePlan{}, fmt.Errorf("list release plan: %w", err)
}
defer rows.Close()
plan := governance.ReleasePlan{Items: []governance.ReleasePlanItem{}}
for rows.Next() {
var item governance.ReleasePlanItem
if err := rows.Scan(&item.PublicationID, &item.Title, &item.TargetScope, &item.Action, &item.TestState, &item.ProductionState, &item.CanRelease, &item.Blocker); err != nil {
return governance.ReleasePlan{}, fmt.Errorf("scan release plan: %w", err)
}
if (item.TargetScope != "internal" && item.TargetScope != "external") || (item.Action != "add" && item.Action != "update" && item.Action != "delete" && item.Action != "blocked" && item.Action != "unchanged") {
return governance.ReleasePlan{}, errors.New("invalid release plan row")
}
switch item.Action {
case "add":
plan.Summary.Add++
case "update":
plan.Summary.Update++
case "delete":
plan.Summary.Delete++
case "unchanged":
plan.Summary.Unchanged++
continue
}
if !item.CanRelease {
plan.Summary.Blocked++
}
plan.Items = append(plan.Items, item)
}
if err := rows.Err(); err != nil {
return governance.ReleasePlan{}, fmt.Errorf("iterate release plan: %w", err)
}
return plan, nil
}