服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
625 lines
21 KiB
Go
625 lines
21 KiB
Go
package servergroup
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
// ErrMergeModuleInvalid 表示合服模块缺少名称、阶段或执行函数。
|
|
ErrMergeModuleInvalid = errors.New("server group merge module is invalid")
|
|
// ErrMergeModuleExists 表示合服模块名称已经注册。
|
|
ErrMergeModuleExists = errors.New("server group merge module already exists")
|
|
// ErrMergeModuleFailed 表示合服模块执行失败。
|
|
ErrMergeModuleFailed = errors.New("server group merge module failed")
|
|
)
|
|
|
|
const (
|
|
// MergeModulePhaseGenDB 表示合服模块处于预生成数据库阶段。
|
|
MergeModulePhaseGenDB = "gendb"
|
|
// MergeModulePhaseMerge 表示合服模块处于正式合并阶段。
|
|
MergeModulePhaseMerge = "merge"
|
|
// MergeModulePhaseRollback 表示合服模块处于回滚阶段。
|
|
MergeModulePhaseRollback = "rollback"
|
|
)
|
|
|
|
// MergeModuleFunc 是项目侧合服模块的统一执行函数。
|
|
type MergeModuleFunc func(context.Context, MergeModuleInput) (MergeModuleResult, error)
|
|
|
|
// MergeGenDBFunc 对应合服前的数据生成和冲突预检阶段。
|
|
type MergeGenDBFunc = MergeModuleFunc
|
|
|
|
// MergeApplyFunc 对应业务数据真正迁移或合并阶段。
|
|
type MergeApplyFunc = MergeModuleFunc
|
|
|
|
// MergeRollbackFunc 对应业务数据回滚或补偿阶段。
|
|
type MergeRollbackFunc = MergeModuleFunc
|
|
|
|
// MergeModuleSpec 描述一个项目侧合服模块,平台只保存通用契约,不保存具体业务表结构。
|
|
type MergeModuleSpec struct {
|
|
// Name 是模块唯一名,建议使用 profile、mail、leaderboard 这类稳定业务域名。
|
|
Name string `json:"name"`
|
|
// Description 描述模块职责,主要用于报告和运营后台展示。
|
|
Description string `json:"description,omitempty"`
|
|
// Features 是模块影响的功能域,用于和合服请求里的 feature 对齐。
|
|
Features []string `json:"features,omitempty"`
|
|
// Required 表示该模块是项目合服的必跑模块,必须同时提供三段处理器。
|
|
Required bool `json:"required"`
|
|
// GenDB 负责合服前生成预检数据、冲突方案和 dry-run 证据。
|
|
GenDB MergeGenDBFunc `json:"-"`
|
|
// Merge 负责合服窗口内的真实业务数据合并。
|
|
Merge MergeApplyFunc `json:"-"`
|
|
// Rollback 负责合服失败后的业务回滚或补偿。
|
|
Rollback MergeRollbackFunc `json:"-"`
|
|
}
|
|
|
|
// MergeModuleSnapshot 是注册器对外暴露的只读模块摘要。
|
|
type MergeModuleSnapshot struct {
|
|
// Name 是模块唯一名。
|
|
Name string `json:"name"`
|
|
// Description 是模块职责说明。
|
|
Description string `json:"description,omitempty"`
|
|
// Features 是模块影响的功能域。
|
|
Features []string `json:"features,omitempty"`
|
|
// Required 表示模块是否为必跑模块。
|
|
Required bool `json:"required"`
|
|
// HasGenDB 表示是否注册了预检处理器。
|
|
HasGenDB bool `json:"has_gendb"`
|
|
// HasMerge 表示是否注册了合并处理器。
|
|
HasMerge bool `json:"has_merge"`
|
|
// HasRollback 表示是否注册了回滚处理器。
|
|
HasRollback bool `json:"has_rollback"`
|
|
}
|
|
|
|
// MergeModuleInput 是传给项目侧合服模块的通用上下文快照。
|
|
type MergeModuleInput struct {
|
|
// Request 是当前合服请求。
|
|
Request MergeRequest `json:"request"`
|
|
// Current 是执行前的平台路由计划快照。
|
|
Current Plan `json:"current"`
|
|
// DryRun 是平台 dry-run 结果,模块侧可以复用其中的回滚点和写检查。
|
|
DryRun MergeDryRun `json:"dry_run"`
|
|
// Steps 是平台迁移步骤快照。
|
|
Steps []MigrationStep `json:"steps,omitempty"`
|
|
// Findings 是前置计划已有的风险项,供后续阶段生成证据时参考。
|
|
Findings []ConflictFinding `json:"findings,omitempty"`
|
|
// Meta 是项目编排器传入的通用元数据。
|
|
Meta map[string]string `json:"meta,omitempty"`
|
|
}
|
|
|
|
// MergeModuleResult 是项目侧模块执行后的通用回执。
|
|
type MergeModuleResult struct {
|
|
// Module 是执行结果所属模块,空值会回填注册器里的模块名。
|
|
Module string `json:"module"`
|
|
// Phase 是执行阶段,取值为 gendb、merge 或 rollback。
|
|
Phase string `json:"phase"`
|
|
// OK 表示该模块阶段没有 blocker finding。
|
|
OK bool `json:"ok"`
|
|
// Skipped 表示模块与当前请求无关,不进入阶段报告。
|
|
Skipped bool `json:"skipped,omitempty"`
|
|
// Findings 是模块返回的冲突、风险或阻断项。
|
|
Findings []ConflictFinding `json:"findings,omitempty"`
|
|
// Evidence 是模块返回的证据路径、批次号或校验摘要。
|
|
Evidence []string `json:"evidence,omitempty"`
|
|
// Rollback 是模块建议的回滚动作或补偿步骤。
|
|
Rollback []string `json:"rollback,omitempty"`
|
|
// Meta 是模块返回的通用元数据。
|
|
Meta map[string]string `json:"meta,omitempty"`
|
|
}
|
|
|
|
// MergeModuleRunReport 汇总一次模块级合服阶段执行结果,用于运营审计和上线证据归档。
|
|
type MergeModuleRunReport struct {
|
|
// Phase 是本次报告对应的阶段,取值为 gendb、merge 或 rollback。
|
|
Phase string `json:"phase"`
|
|
// Request 是合服请求快照,便于证据脱离执行进程后仍能复核。
|
|
Request MergeRequest `json:"request"`
|
|
// GeneratedAt 是报告生成时间,统一保存为 UTC。
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
// OK 表示本阶段没有 blocker finding,业务错误应直接返回 error。
|
|
OK bool `json:"ok"`
|
|
// Results 保存每个模块的原始阶段回执。
|
|
Results []MergeModuleResult `json:"results,omitempty"`
|
|
// Findings 汇总所有模块返回的冲突或风险项。
|
|
Findings []ConflictFinding `json:"findings,omitempty"`
|
|
// Evidence 汇总所有模块返回的证据路径、批次号或校验摘要。
|
|
Evidence []string `json:"evidence,omitempty"`
|
|
// Rollback 汇总所有模块声明的回滚动作或补偿步骤。
|
|
Rollback []string `json:"rollback,omitempty"`
|
|
// Meta 保存项目编排器附加的通用元数据。
|
|
Meta map[string]string `json:"meta,omitempty"`
|
|
}
|
|
|
|
// MergeModuleCoverageReport 描述合服请求 feature 与模块处理器之间的覆盖关系。
|
|
type MergeModuleCoverageReport struct {
|
|
// Phase 是本次覆盖检查对应的阶段。
|
|
Phase string `json:"phase"`
|
|
// Request 是被检查的合服请求快照。
|
|
Request MergeRequest `json:"request"`
|
|
// OK 表示所有声明的 feature 都有对应阶段处理器覆盖。
|
|
OK bool `json:"ok"`
|
|
// Features 是本次请求声明需要检查或冻结的功能域。
|
|
Features []string `json:"features,omitempty"`
|
|
// Missing 是缺少模块处理器覆盖的功能域。
|
|
Missing []string `json:"missing,omitempty"`
|
|
// Modules 是注册器当前模块快照。
|
|
Modules []MergeModuleSnapshot `json:"modules,omitempty"`
|
|
// Findings 是缺失覆盖转换出的 blocker 风险项。
|
|
Findings []ConflictFinding `json:"findings,omitempty"`
|
|
}
|
|
|
|
// MergeOpRegistry 把成熟项目里按模块注册 GenDB/Merge/Rollback 的经验抽成龙恒通用扩展点。
|
|
type MergeOpRegistry struct {
|
|
mu sync.RWMutex
|
|
modules map[string]MergeModuleSpec
|
|
}
|
|
|
|
// NewMergeOpRegistry 创建模块注册器,并按注册顺序校验重复和必填处理器。
|
|
func NewMergeOpRegistry(specs ...MergeModuleSpec) (*MergeOpRegistry, error) {
|
|
registry := &MergeOpRegistry{modules: make(map[string]MergeModuleSpec, len(specs))}
|
|
for _, spec := range specs {
|
|
if err := registry.Register(spec); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return registry, nil
|
|
}
|
|
|
|
// Register 注册一个项目侧合服模块,重复模块名会被拒绝。
|
|
func (r *MergeOpRegistry) Register(spec MergeModuleSpec) error {
|
|
if r == nil {
|
|
return fmt.Errorf("%w: registry is nil", ErrMergeModuleInvalid)
|
|
}
|
|
normalized, err := normMergeSpec(spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, exists := r.modules[normalized.Name]; exists {
|
|
return fmt.Errorf("%w: %s", ErrMergeModuleExists, normalized.Name)
|
|
}
|
|
r.modules[normalized.Name] = normalized
|
|
return nil
|
|
}
|
|
|
|
// Snapshot 返回按模块名排序的注册器快照,调用方可以安全修改返回值。
|
|
func (r *MergeOpRegistry) Snapshot() []MergeModuleSnapshot {
|
|
specs := r.snapshotSpecs()
|
|
if len(specs) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]MergeModuleSnapshot, 0, len(specs))
|
|
for _, spec := range specs {
|
|
out = append(out, MergeModuleSnapshot{
|
|
Name: spec.Name,
|
|
Description: spec.Description,
|
|
Features: append([]string(nil), spec.Features...),
|
|
Required: spec.Required,
|
|
HasGenDB: spec.GenDB != nil,
|
|
HasMerge: spec.Merge != nil,
|
|
HasRollback: spec.Rollback != nil,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// BuildChecker 把注册器转换为合服迁移计划可用的冲突检查器。
|
|
func (r *MergeOpRegistry) BuildChecker() ConflictChecker {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return ConflictCheckFunc(func(ctx context.Context, request ConflictCheckRequest) ([]ConflictFinding, error) {
|
|
coverage, err := r.CheckCoverage(request.DryRun.Request, MergeModulePhaseGenDB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results, err := r.RunGenDB(ctx, MergeModuleInput{
|
|
Request: request.DryRun.Request,
|
|
Current: request.Current,
|
|
DryRun: request.DryRun,
|
|
Steps: request.Steps,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
findings := cloneClashFindings(coverage.Findings)
|
|
for _, result := range results {
|
|
findings = append(findings, result.Findings...)
|
|
}
|
|
return findings, nil
|
|
})
|
|
}
|
|
|
|
// CheckCoverage 检查合服请求声明的 feature 是否有对应阶段模块处理器覆盖。
|
|
func (r *MergeOpRegistry) CheckCoverage(request MergeRequest, phase string) (MergeModuleCoverageReport, error) {
|
|
phase = normalizeID(phase)
|
|
if !validMergePhase(phase) {
|
|
return MergeModuleCoverageReport{}, fmt.Errorf("%w: invalid coverage phase %s", ErrMergeModuleInvalid, phase)
|
|
}
|
|
request = normMergeReq(request)
|
|
specs := r.snapshotSpecs()
|
|
features := requestedFeatures(request)
|
|
report := MergeModuleCoverageReport{
|
|
Phase: phase,
|
|
Request: request,
|
|
OK: true,
|
|
Features: features,
|
|
Modules: snapshotsFromSpecs(specs),
|
|
}
|
|
if len(features) == 0 || !hasFeatureDecls(specs) {
|
|
return report, nil
|
|
}
|
|
for _, feature := range features {
|
|
if mergeFeatureCovered(specs, phase, feature) {
|
|
continue
|
|
}
|
|
report.Missing = append(report.Missing, feature)
|
|
report.Findings = append(report.Findings, ConflictFinding{
|
|
Code: "merge_module_feature_missing",
|
|
Severity: MigrationSeverityBlocker,
|
|
Subject: feature,
|
|
Detail: fmt.Sprintf("合服 feature %s 缺少 %s 阶段模块处理器", feature, phase),
|
|
})
|
|
}
|
|
report.Missing = normalizeIDs(report.Missing)
|
|
report.Findings = cloneClashFindings(report.Findings)
|
|
report.OK = len(report.Missing) == 0
|
|
return report, nil
|
|
}
|
|
|
|
// CoverageFindings 返回覆盖缺口对应的 blocker findings,便于接入自定义计划检查器。
|
|
func (r *MergeOpRegistry) CoverageFindings(request MergeRequest, phase string) ([]ConflictFinding, error) {
|
|
report, err := r.CheckCoverage(request, phase)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cloneClashFindings(report.Findings), nil
|
|
}
|
|
|
|
// RunGenDB 执行所有已注册的合服前数据生成和冲突预检模块。
|
|
func (r *MergeOpRegistry) RunGenDB(ctx context.Context, input MergeModuleInput) ([]MergeModuleResult, error) {
|
|
return r.runPhase(ctx, MergeModulePhaseGenDB, input)
|
|
}
|
|
|
|
// RunGenDBReport 执行合服前数据生成和冲突预检,并生成可归档报告。
|
|
func (r *MergeOpRegistry) RunGenDBReport(ctx context.Context, input MergeModuleInput) (MergeModuleRunReport, error) {
|
|
return r.runPhaseReport(ctx, MergeModulePhaseGenDB, input)
|
|
}
|
|
|
|
// RunMerge 执行所有已注册的业务合并模块。
|
|
func (r *MergeOpRegistry) RunMerge(ctx context.Context, input MergeModuleInput) ([]MergeModuleResult, error) {
|
|
return r.runPhase(ctx, MergeModulePhaseMerge, input)
|
|
}
|
|
|
|
// RunMergeReport 执行业务合并模块,并生成可归档报告。
|
|
func (r *MergeOpRegistry) RunMergeReport(ctx context.Context, input MergeModuleInput) (MergeModuleRunReport, error) {
|
|
return r.runPhaseReport(ctx, MergeModulePhaseMerge, input)
|
|
}
|
|
|
|
// RunRollback 执行所有已注册的业务回滚模块。
|
|
func (r *MergeOpRegistry) RunRollback(ctx context.Context, input MergeModuleInput) ([]MergeModuleResult, error) {
|
|
return r.runPhase(ctx, MergeModulePhaseRollback, input)
|
|
}
|
|
|
|
// RunRollbackReport 执行业务回滚模块,并生成可归档报告。
|
|
func (r *MergeOpRegistry) RunRollbackReport(ctx context.Context, input MergeModuleInput) (MergeModuleRunReport, error) {
|
|
return r.runPhaseReport(ctx, MergeModulePhaseRollback, input)
|
|
}
|
|
|
|
// MergeModuleInputFromPlan 根据平台迁移计划生成项目侧模块输入,便于业务编排器复用同一份 dry-run 证据。
|
|
func MergeModuleInputFromPlan(current Plan, plan MigrationPlan) MergeModuleInput {
|
|
return MergeModuleInput{
|
|
Request: plan.Request,
|
|
Current: current,
|
|
DryRun: plan.DryRun,
|
|
Steps: plan.Steps,
|
|
Findings: plan.Findings,
|
|
}
|
|
}
|
|
|
|
func (r *MergeOpRegistry) runPhase(ctx context.Context, phase string, input MergeModuleInput) ([]MergeModuleResult, error) {
|
|
if err := ctxErr(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
phase = normalizeID(phase)
|
|
if !validMergePhase(phase) {
|
|
return nil, fmt.Errorf("%w: invalid phase %s", ErrMergeModuleInvalid, phase)
|
|
}
|
|
specs := r.snapshotSpecs()
|
|
if len(specs) == 0 {
|
|
return nil, nil
|
|
}
|
|
input = cloneMergeInput(input)
|
|
results := make([]MergeModuleResult, 0, len(specs))
|
|
for _, spec := range specs {
|
|
if err := ctxErr(ctx); err != nil {
|
|
return results, err
|
|
}
|
|
fn := mergeFuncForPhase(spec, phase)
|
|
if fn == nil {
|
|
continue
|
|
}
|
|
result, err := fn(ctx, cloneMergeInput(input))
|
|
if err != nil {
|
|
return results, fmt.Errorf("%w: %s %s: %w", ErrMergeModuleFailed, spec.Name, phase, err)
|
|
}
|
|
normalized, err := normMergeResult(spec.Name, phase, result)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
if normalized.Skipped {
|
|
continue
|
|
}
|
|
results = append(results, normalized)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (r *MergeOpRegistry) runPhaseReport(ctx context.Context, phase string, input MergeModuleInput) (MergeModuleRunReport, error) {
|
|
coverage, err := r.CheckCoverage(mergeInputReq(input), phase)
|
|
if err != nil {
|
|
return MergeModuleRunReport{}, err
|
|
}
|
|
results, err := r.runPhase(ctx, phase, input)
|
|
if err != nil {
|
|
return MergeModuleRunReport{}, err
|
|
}
|
|
report, err := newMergeRunReport(phase, input, results, time.Now().UTC())
|
|
if err != nil {
|
|
return MergeModuleRunReport{}, err
|
|
}
|
|
report.Findings = append(cloneClashFindings(coverage.Findings), report.Findings...)
|
|
if !coverage.OK {
|
|
report.OK = false
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
func (r *MergeOpRegistry) snapshotSpecs() []MergeModuleSpec {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
if len(r.modules) == 0 {
|
|
return nil
|
|
}
|
|
names := make([]string, 0, len(r.modules))
|
|
for name := range r.modules {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
out := make([]MergeModuleSpec, 0, len(names))
|
|
for _, name := range names {
|
|
out = append(out, cloneMergeModuleSpec(r.modules[name]))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normMergeSpec(spec MergeModuleSpec) (MergeModuleSpec, error) {
|
|
spec.Name = normalizeID(spec.Name)
|
|
spec.Description = firstNonEmpty(spec.Description)
|
|
spec.Features = normalizeIDs(spec.Features)
|
|
if spec.Name == "" {
|
|
return MergeModuleSpec{}, fmt.Errorf("%w: module name is required", ErrMergeModuleInvalid)
|
|
}
|
|
if spec.GenDB == nil && spec.Merge == nil && spec.Rollback == nil {
|
|
return MergeModuleSpec{}, fmt.Errorf("%w: module %s has no handlers", ErrMergeModuleInvalid, spec.Name)
|
|
}
|
|
if spec.Required && (spec.GenDB == nil || spec.Merge == nil || spec.Rollback == nil) {
|
|
return MergeModuleSpec{}, fmt.Errorf("%w: required module %s must provide gendb, merge and rollback handlers", ErrMergeModuleInvalid, spec.Name)
|
|
}
|
|
return spec, nil
|
|
}
|
|
|
|
func snapshotsFromSpecs(specs []MergeModuleSpec) []MergeModuleSnapshot {
|
|
if len(specs) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]MergeModuleSnapshot, 0, len(specs))
|
|
for _, spec := range specs {
|
|
out = append(out, MergeModuleSnapshot{
|
|
Name: spec.Name,
|
|
Description: spec.Description,
|
|
Features: append([]string(nil), spec.Features...),
|
|
Required: spec.Required,
|
|
HasGenDB: spec.GenDB != nil,
|
|
HasMerge: spec.Merge != nil,
|
|
HasRollback: spec.Rollback != nil,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneMergeModuleSpec(spec MergeModuleSpec) MergeModuleSpec {
|
|
spec.Features = append([]string(nil), spec.Features...)
|
|
return spec
|
|
}
|
|
|
|
func mergeFuncForPhase(spec MergeModuleSpec, phase string) MergeModuleFunc {
|
|
switch phase {
|
|
case MergeModulePhaseGenDB:
|
|
return spec.GenDB
|
|
case MergeModulePhaseMerge:
|
|
return spec.Merge
|
|
case MergeModulePhaseRollback:
|
|
return spec.Rollback
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func validMergePhase(phase string) bool {
|
|
switch phase {
|
|
case MergeModulePhaseGenDB, MergeModulePhaseMerge, MergeModulePhaseRollback:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func requestedFeatures(request MergeRequest) []string {
|
|
request = normMergeReq(request)
|
|
features := append([]string(nil), request.CheckFeatures...)
|
|
features = append(features, request.BlockFeatures...)
|
|
return normalizeIDs(features)
|
|
}
|
|
|
|
func hasFeatureDecls(specs []MergeModuleSpec) bool {
|
|
for _, spec := range specs {
|
|
if len(spec.Features) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mergeFeatureCovered(specs []MergeModuleSpec, phase, feature string) bool {
|
|
feature = normalizeID(feature)
|
|
for _, spec := range specs {
|
|
if len(spec.Features) == 0 || !containsID(spec.Features, feature) {
|
|
continue
|
|
}
|
|
if mergeFuncForPhase(spec, phase) != nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mergeInputReq(input MergeModuleInput) MergeRequest {
|
|
request := normMergeReq(input.Request)
|
|
if request.ID != "" {
|
|
return request
|
|
}
|
|
return normMergeReq(input.DryRun.Request)
|
|
}
|
|
|
|
func cloneMergeInput(input MergeModuleInput) MergeModuleInput {
|
|
return MergeModuleInput{
|
|
Request: mergeInputReq(input),
|
|
Current: clonePlan(input.Current),
|
|
DryRun: cloneMergeDryRun(input.DryRun),
|
|
Steps: cloneMigrationSteps(input.Steps),
|
|
Findings: cloneClashFindings(input.Findings),
|
|
Meta: normalizeStringMap(input.Meta),
|
|
}
|
|
}
|
|
|
|
func normMergeResult(module, phase string, result MergeModuleResult) (MergeModuleResult, error) {
|
|
result.Module = firstNonEmpty(result.Module, module)
|
|
result.Phase = firstNonEmpty(result.Phase, phase)
|
|
result.Evidence = cloneMergeStrings(result.Evidence)
|
|
result.Rollback = cloneMergeStrings(result.Rollback)
|
|
result.Meta = normalizeStringMap(result.Meta)
|
|
if result.Module == "" || result.Phase == "" {
|
|
return MergeModuleResult{}, fmt.Errorf("%w: result module and phase are required", ErrMergeModuleInvalid)
|
|
}
|
|
if !validMergePhase(result.Phase) {
|
|
return MergeModuleResult{}, fmt.Errorf("%w: result %s has invalid phase %s", ErrMergeModuleInvalid, result.Module, result.Phase)
|
|
}
|
|
findings, err := normalizeFindings(result.Module, result.Findings)
|
|
if err != nil {
|
|
return MergeModuleResult{}, err
|
|
}
|
|
result.Findings = findings
|
|
result.OK = !hasBlockerFinding(result.Findings)
|
|
return result, nil
|
|
}
|
|
|
|
func newMergeRunReport(phase string, input MergeModuleInput, results []MergeModuleResult, generatedAt time.Time) (MergeModuleRunReport, error) {
|
|
phase = normalizeID(phase)
|
|
if !validMergePhase(phase) {
|
|
return MergeModuleRunReport{}, fmt.Errorf("%w: invalid report phase %s", ErrMergeModuleInvalid, phase)
|
|
}
|
|
if generatedAt.IsZero() {
|
|
generatedAt = time.Now().UTC()
|
|
} else {
|
|
generatedAt = generatedAt.UTC()
|
|
}
|
|
input = cloneMergeInput(input)
|
|
results = cloneMergeResults(results)
|
|
report := MergeModuleRunReport{
|
|
Phase: phase,
|
|
Request: input.Request,
|
|
GeneratedAt: generatedAt,
|
|
OK: true,
|
|
Results: results,
|
|
Meta: cloneStringMap(input.Meta),
|
|
}
|
|
for _, result := range results {
|
|
report.Findings = append(report.Findings, result.Findings...)
|
|
report.Evidence = append(report.Evidence, result.Evidence...)
|
|
report.Rollback = append(report.Rollback, result.Rollback...)
|
|
if !result.OK {
|
|
report.OK = false
|
|
}
|
|
}
|
|
report.Findings = cloneClashFindings(report.Findings)
|
|
report.Evidence = cloneMergeStrings(report.Evidence)
|
|
report.Rollback = cloneMergeStrings(report.Rollback)
|
|
if hasBlockerFinding(report.Findings) {
|
|
report.OK = false
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
func normalizeFindings(module string, findings []ConflictFinding) ([]ConflictFinding, error) {
|
|
if len(findings) == 0 {
|
|
return nil, nil
|
|
}
|
|
out := make([]ConflictFinding, 0, len(findings))
|
|
for _, finding := range findings {
|
|
if finding.Subject == "" {
|
|
finding.Subject = module
|
|
}
|
|
normalized, err := normConflict(finding)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, normalized)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func cloneMergeResults(results []MergeModuleResult) []MergeModuleResult {
|
|
if len(results) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]MergeModuleResult, len(results))
|
|
for i, result := range results {
|
|
out[i] = MergeModuleResult{
|
|
Module: result.Module,
|
|
Phase: result.Phase,
|
|
OK: result.OK,
|
|
Skipped: result.Skipped,
|
|
Findings: cloneClashFindings(result.Findings),
|
|
Evidence: cloneMergeStrings(result.Evidence),
|
|
Rollback: cloneMergeStrings(result.Rollback),
|
|
Meta: cloneStringMap(result.Meta),
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneMergeStrings(values []string) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
value = firstNonEmpty(value)
|
|
if value != "" {
|
|
out = append(out, value)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|