package report

import (
	"crypto/sha256"
	"errors"
	"fmt"
	"math"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/Clyra-AI/wrkr/core/aggregate/agentresolver"
	aggattack "github.com/Clyra-AI/wrkr/core/aggregate/attackpath"
	"github.com/Clyra-AI/wrkr/core/aggregate/controlbacklog"
	agginventory "github.com/Clyra-AI/wrkr/core/aggregate/inventory"
	"github.com/Clyra-AI/wrkr/core/aggregate/scanquality"
	"github.com/Clyra-AI/wrkr/core/compliance"
	"github.com/Clyra-AI/wrkr/core/detect"
	"github.com/Clyra-AI/wrkr/core/governancequeue"
	"github.com/Clyra-AI/wrkr/core/identity"
	"github.com/Clyra-AI/wrkr/core/ingest"
	"github.com/Clyra-AI/wrkr/core/lifecycle"
	"github.com/Clyra-AI/wrkr/core/manifest"
	"github.com/Clyra-AI/wrkr/core/model"
	"github.com/Clyra-AI/wrkr/core/proofemit"
	"github.com/Clyra-AI/wrkr/core/regress"
	templatespkg "github.com/Clyra-AI/wrkr/core/report/templates"
	"github.com/Clyra-AI/wrkr/core/risk"
	riskattack "github.com/Clyra-AI/wrkr/core/risk/attackpath"
	scorecore "github.com/Clyra-AI/wrkr/core/score"
	"github.com/Clyra-AI/wrkr/core/source"
	"github.com/Clyra-AI/wrkr/core/sourceprivacy"
	"github.com/Clyra-AI/wrkr/core/state"
	verifycore "github.com/Clyra-AI/wrkr/core/verify"
)

// BuildSummary composes deterministic report sections from scan, risk, score, lifecycle, regress, and proof data.
// Non-goal guardrail: this path must remain deterministic and non-generative.
func BuildSummary(in BuildInput) (Summary, error) {
	template := in.Template
	if template == "" {
		template = TemplateOperator
	}
	if _, ok := ParseTemplate(string(template)); !ok {
		return Summary{}, fmt.Errorf("unsupported report template %q", template)
	}

	shareProfile := in.ShareProfile
	if shareProfile == "" {
		shareProfile = ShareProfileInternal
	}
	if template == TemplateCustomerDraft {
		shareProfile = ShareProfilePublic
	}
	if template == TemplateDesignPartnerSummary && in.ShareProfile == "" {
		shareProfile = ShareProfileDesignPartner
	}
	if _, ok := ParseShareProfile(string(shareProfile)); !ok {
		return Summary{}, fmt.Errorf("unsupported share profile %q", shareProfile)
	}
	redactionConfig := ResolveRedactionConfig(shareProfile, in.RedactionFields)

	now := resolveGeneratedAt(in.GeneratedAt, in.Snapshot)
	top := in.Top

	riskReport := in.Snapshot.RiskReport
	if riskReport == nil {
		generated := risk.Score(in.Snapshot.Findings, top, now)
		riskReport = &generated
	}
	if len(riskReport.ActionPaths) == 0 && in.Snapshot.Inventory != nil {
		riskReport.ActionPaths, riskReport.ActionPathToControlFirst = risk.BuildActionPaths(riskReport.AttackPaths, in.Snapshot.Inventory)
	}
	profileName := ""
	if in.Snapshot.Profile != nil {
		profileName = in.Snapshot.Profile.ProfileName
	}
	riskReport.ActionPaths, riskReport.ActionPathToControlFirst = risk.ApplyGovernFirstProfile(profileName, riskReport.ActionPaths)
	topFindings := SelectTopFindings(*riskReport, top)

	proofRef, err := buildProofReference(in.StatePath, topFindings)
	if err != nil {
		return Summary{}, err
	}
	complianceSummary, err := buildComplianceSummary(in.StatePath, in.Snapshot.Findings)
	if err != nil {
		return Summary{}, err
	}

	lifecycleSummary := buildLifecycleSummary(in.Manifest, in.Snapshot.Identities, in.Snapshot.Transitions, in.Snapshot.LifecycleGaps, in.Snapshot.Inventory)
	regressSummary := buildRegressSummary(in.Baseline, in.RegressResult)
	deltas := buildDeltaSummary(in.Snapshot, in.PreviousSnapshot, top)
	headline := buildHeadline(in.Snapshot)
	methodology := buildMethodology(in.Snapshot)
	scanScope := buildScanScopeSummary(in.Snapshot)
	attackPathSummary := buildAttackPathSummary(*riskReport)
	attackPathFacts := buildAttackPathFacts(*riskReport)
	scanQuality := cloneScanQualityReport(in.Snapshot.ScanQuality)
	if scanQuality != nil && scanQuality.CompactSummary == nil {
		compact := scanquality.BuildCompactCoverageSummary(scanQuality)
		scanQuality.CompactSummary = &compact
	}
	sourcePrivacy := normalizedSourcePrivacy(in.Snapshot.SourcePrivacy)
	deploymentMode := resolveDeploymentMode(sourcePrivacy)
	runtimeSessions := buildSessionSummary(in.StatePath, in.Snapshot)
	runtimeEvidence := buildRuntimeEvidenceSummary(in.StatePath, in.Snapshot)
	evidencePackets := buildEvidencePacketSummary(in.StatePath, in.Snapshot)
	publicSurfaceAssessment := buildPublicSurfaceAssessment(in.Snapshot.PublicEvidenceManifestName, in.Snapshot.PublicEvidence)
	riskReport.ActionPaths = decorateActionPathsForReport(riskReport.ActionPaths, runtimeEvidence)
	riskReport.ActionPaths = decorateActionPathsForEvidencePackets(riskReport.ActionPaths, evidencePackets)
	riskReport.ActionPaths = decorateActionPathsForEnterpriseContext(riskReport.ActionPaths, runtimeSessions, evidencePackets)
	riskReport.ActionPaths = risk.DecorateEvidenceContext(riskReport.ActionPaths, scanQuality)
	riskReport.ActionPaths = decorateBoundaryLabelsForReport(
		riskReport.ActionPaths,
		runtimeEvidenceByPath(runtimeEvidence),
		runtimeSessionsByPath(runtimeSessions),
		evidencePacketsByPath(evidencePackets),
	)
	if len(riskReport.ActionPaths) > 0 {
		riskReport.ControlPathGraph = risk.BuildControlPathGraph(riskReport.ActionPaths)
		riskReport.ControlPathGraph = decorateControlPathGraphBoundary(riskReport.ControlPathGraph, riskReport.ActionPaths)
		riskReport.WorkflowChains = risk.BuildWorkflowChains(riskReport.ActionPaths, riskReport.ControlPathGraph)
		riskReport.ActionPaths = risk.DecorateWorkflowChainRefs(riskReport.ActionPaths, riskReport.WorkflowChains)
		riskReport.ActionPaths = risk.DecorateActionLineage(riskReport.ActionPaths, riskReport.ControlPathGraph)
	}
	runtimeSessions = decorateSessionSummaryBoundary(runtimeSessions, riskReport.ActionPaths)
	runtimeEvidence = decorateRuntimeEvidenceSummaryBoundary(runtimeEvidence, riskReport.ActionPaths)
	evidencePackets = decorateEvidencePacketSummaryBoundary(evidencePackets, riskReport.ActionPaths)
	riskReport.ActionPathToControlFirst = decorateControlFirstForReport(riskReport.ActionPaths, scanQualityCoverageReduced(scanQuality))
	activation := BuildActivation(in.Snapshot.Target.Mode, riskReport.Ranked, in.Snapshot.Inventory, riskReport.ActionPaths, top)
	exposureGroups := risk.BuildExposureGroups(riskReport.ActionPaths)
	controlBacklogBuilt := controlbacklog.Build(controlbacklog.Input{
		Mode:             in.Snapshot.Target.Mode,
		GeneratedAt:      now,
		Findings:         in.Snapshot.Findings,
		Inventory:        in.Snapshot.Inventory,
		Identities:       in.Snapshot.Identities,
		LifecycleGaps:    in.Snapshot.LifecycleGaps,
		ActionPaths:      riskReport.ActionPaths,
		ControlPathGraph: riskReport.ControlPathGraph,
	})
	var controlBacklog *controlbacklog.Backlog
	if in.Snapshot.ControlBacklog != nil {
		controlBacklog = mergeBacklogGovernance(decorateControlBacklogFromActionPaths(in.Snapshot.ControlBacklog, riskReport.ActionPaths), &controlBacklogBuilt)
	} else {
		controlBacklog = &controlBacklogBuilt
	}
	riskItems := buildRiskItems(topFindings, riskReport.ActionPaths)
	assessmentSummary := buildAssessmentSummary(riskReport.ActionPaths, riskReport.ActionPathToControlFirst, in.Snapshot.Inventory, proofRef)
	rawControlPathGraph := riskReport.ControlPathGraph
	rawWorkflowChains := riskReport.WorkflowChains
	controlProofStatus, err := buildControlProofStatusForSummary(in.StatePath, in.Snapshot, riskReport)
	if err != nil {
		return Summary{}, err
	}
	decisionTraceRefsByPath, err := buildDecisionTraceRefsForSummary(in.StatePath)
	if err != nil {
		return Summary{}, err
	}
	riskReport.ActionPaths = decorateDecisionTraceRefs(riskReport.ActionPaths, decisionTraceRefsByPath)
	riskReport.ActionPaths = decorateActionPathsWithPrecedents(in.StatePath, now, riskReport.ActionPaths)
	riskReport.ActionPathToControlFirst = risk.BuildActionPathChoice(riskReport.ActionPaths)
	rawActionPaths := append([]risk.ActionPath(nil), riskReport.ActionPaths...)
	rawActionPathToControlFirst := riskReport.ActionPathToControlFirst
	controlBacklog = decorateControlBacklogDecisionPrecedents(controlBacklog, riskReport.ActionPaths)
	shareProfileMetadata := BuildShareProfileMetadata(redactionConfig)
	operationalExposure := scorecore.SummarizeOperationalExposure(riskReport.ActionPaths)
	governanceReadiness := scorecore.SummarizeGovernanceReadiness(riskReport.ActionPaths, missingProofPathCount(controlProofStatus), scanQualityCoverageReduced(scanQuality))
	evidenceCompleteness := risk.BuildEvidenceCompletenessSummary(riskReport.ActionPaths)

	if shareProfileRequiresRedaction(shareProfile) {
		proofRef = sanitizeProofReferencePublic(proofRef)
		lifecycleSummary = sanitizeLifecycleSummaryPublic(lifecycleSummary)
		riskItems = sanitizeRiskItemsPublic(riskItems)
		activation = sanitizeActivationSummaryPublic(activation)
		riskReport.ActionPaths = sanitizeActionPathsPublic(riskReport.ActionPaths)
		riskReport.ActionPathToControlFirst = sanitizeActionPathToControlFirstPublic(riskReport.ActionPathToControlFirst)
		riskReport.ControlPathGraph = sanitizeControlPathGraphPublic(riskReport.ControlPathGraph)
		riskReport.WorkflowChains = sanitizeWorkflowChainsPublic(riskReport.WorkflowChains)
		exposureGroups = sanitizeExposureGroupsPublic(exposureGroups)
		assessmentSummary = sanitizeAssessmentSummaryPublic(assessmentSummary)
		controlBacklog = sanitizeControlBacklogPublic(controlBacklog)
		scanQuality = sanitizeScanQualityPublic(scanQuality)
		runtimeSessions = sanitizeSessionSummaryPublic(runtimeSessions, rawActionPaths, riskReport.ActionPaths)
		controlProofStatus = sanitizeControlProofStatusPublic(controlProofStatus, rawActionPaths, riskReport.ActionPaths)
		evidencePackets = sanitizeEvidencePacketSummaryPublic(evidencePackets, rawActionPaths, riskReport.ActionPaths)
	} else if redactionConfig.Applies() {
		proofRef = sanitizeProofReferenceWithConfig(proofRef, redactionConfig)
		lifecycleSummary = sanitizeLifecycleSummaryWithConfig(lifecycleSummary, redactionConfig)
		riskItems = sanitizeRiskItemsWithConfig(riskItems, redactionConfig)
		activation = sanitizeActivationSummaryWithConfig(activation, redactionConfig)
		riskReport.ActionPaths = sanitizeActionPathsWithConfig(riskReport.ActionPaths, redactionConfig)
		riskReport.ActionPathToControlFirst = sanitizeActionPathToControlFirstWithConfig(riskReport.ActionPathToControlFirst, redactionConfig)
		riskReport.ControlPathGraph = sanitizeControlPathGraphWithConfig(riskReport.ControlPathGraph, redactionConfig)
		riskReport.WorkflowChains = sanitizeWorkflowChainsWithConfig(riskReport.WorkflowChains, redactionConfig)
		exposureGroups = sanitizeExposureGroupsWithConfig(exposureGroups, redactionConfig)
		assessmentSummary = sanitizeAssessmentSummaryWithConfig(assessmentSummary, redactionConfig)
		controlBacklog = sanitizeControlBacklogWithConfig(controlBacklog, redactionConfig)
		scanQuality = sanitizeScanQualityWithConfig(scanQuality, redactionConfig)
		runtimeSessions = sanitizeSessionSummaryWithConfig(runtimeSessions, rawActionPaths, riskReport.ActionPaths, redactionConfig)
		controlProofStatus = sanitizeControlProofStatusWithConfig(controlProofStatus, rawActionPaths, riskReport.ActionPaths, redactionConfig)
		evidencePackets = sanitizeEvidencePacketSummaryWithConfig(evidencePackets, rawActionPaths, riskReport.ActionPaths, redactionConfig)
	}

	privilegeBudget := privilegeBudgetFromInventory(in.Snapshot.Inventory)
	securityVisibility := securityVisibilityFromInventory(in.Snapshot.Inventory)
	nextActions := buildNextActions(riskItems, lifecycleSummary, regressSummary)
	policyOutcomes := BuildPolicyOutcomes(in.Snapshot.Findings)
	if redactionConfig.Applies() {
		policyOutcomes = sanitizePolicyOutcomesWithConfig(policyOutcomes, redactionConfig)
	}
	pack := templatespkg.Resolve(string(template))
	sections := buildSections(pack, template == TemplatePublic, headline, methodology, riskItems, attackPathFacts, riskReport.ControlPathGraph, complianceSummary, privilegeBudget, securityVisibility, deltas, lifecycleSummary, regressSummary, proofRef, nextActions, sourcePrivacy)

	sectionOrder := []string{
		SectionHeadline,
		SectionTopRisks,
		SectionChanges,
		SectionLifecycle,
		SectionProof,
		SectionNextAction,
	}
	if template == TemplatePublic {
		sectionOrder = []string{
			SectionHeadline,
			SectionMethodology,
			SectionTopRisks,
			SectionChanges,
			SectionLifecycle,
			SectionProof,
			SectionNextAction,
		}
	}

	summary := Summary{
		SummaryVersion:           SummaryVersion,
		GeneratedAt:              now.Format(time.RFC3339),
		Template:                 string(template),
		ShareProfile:             string(shareProfile),
		DeploymentMode:           deploymentMode,
		ShareProfileMetadata:     shareProfileMetadata,
		SectionOrder:             sectionOrder,
		Sections:                 sections,
		Headline:                 headline,
		ScanScope:                scanScope,
		OperationalExposure:      &operationalExposure,
		GovernanceReadiness:      &governanceReadiness,
		EvidenceCompleteness:     evidenceCompleteness,
		AssessmentSummary:        assessmentSummary,
		PublicSurfaceAssessment:  publicSurfaceAssessment,
		Methodology:              methodology,
		TopRisks:                 riskItems,
		PrivilegeBudget:          privilegeBudget,
		SecurityVisibility:       securityVisibility,
		Deltas:                   deltas,
		Lifecycle:                lifecycleSummary,
		RegressDrift:             regressSummary,
		AttackPaths:              attackPathSummary,
		ComplianceSummary:        complianceSummary,
		ControlBacklog:           controlBacklog,
		ScanQuality:              scanQuality,
		RuntimeSessions:          runtimeSessions,
		RuntimeEvidence:          runtimeEvidence,
		EvidencePackets:          evidencePackets,
		Proof:                    proofRef,
		NextActions:              nextActions,
		Activation:               activation,
		PolicyOutcomes:           policyOutcomes,
		ActionPaths:              riskReport.ActionPaths,
		ActionPathToControlFirst: riskReport.ActionPathToControlFirst,
		ControlPathGraph:         riskReport.ControlPathGraph,
		WorkflowChains:           riskReport.WorkflowChains,
		ExposureGroups:           exposureGroups,
		SourcePrivacy:            sourcePrivacy,
		controlProofStatus:       controlProofStatus,
		decisionTraceRefsByPath:  decisionTraceRefsByPath,
		topAttackPaths:           append([]riskattack.ScoredPath(nil), riskReport.TopAttackPaths...),
	}
	bomSource := summary
	bomSource.ActionPaths = append([]risk.ActionPath(nil), rawActionPaths...)
	bomSource.ActionPathToControlFirst = rawActionPathToControlFirst
	bomSource.ControlPathGraph = rawControlPathGraph
	bomSource.WorkflowChains = rawWorkflowChains
	summary.AgentActionBOM = buildAgentActionBOMFromSnapshot(bomSource, in.Snapshot)
	registrySource := bomSource
	registrySource.AgentActionBOM = summary.AgentActionBOM
	summary.ActionSurfaceRegistry = BuildActionSurfaceRegistry(registrySource)
	if shareProfileRequiresRedaction(shareProfile) {
		summary.ActionSurfaceRegistry = sanitizeActionSurfaceRegistryPublic(summary.ActionSurfaceRegistry)
		summary.AgentActionBOM = sanitizeAgentActionBOM(summary.AgentActionBOM, shareProfile)
	} else if redactionConfig.Applies() {
		summary.ActionSurfaceRegistry = sanitizeActionSurfaceRegistryWithConfig(summary.ActionSurfaceRegistry, redactionConfig)
		summary.AgentActionBOM = sanitizeAgentActionBOMWithConfig(summary.AgentActionBOM, shareProfile, redactionConfig)
	}
	summary.ExecutiveRollup = resolveExecutiveRollup(summary)
	summary.GovernedUsageMetrics = resolveGovernedUsageMetrics(summary)
	summary.RepeatUsageSignals = BuildRepeatUsageSignals(in.StatePath)
	if summary.ControlBacklog != nil {
		summary.ControlBacklog.ExecutiveRollup = summary.ExecutiveRollup
		summary.ControlBacklog.GovernedUsageMetrics = summary.GovernedUsageMetrics
	}
	if summary.AgentActionBOM != nil {
		summary.AgentActionBOM.Summary.ExecutiveRollup = summary.ExecutiveRollup
		summary.AgentActionBOM.Summary.GovernedUsageMetrics = summary.GovernedUsageMetrics
		summary.AgentActionBOM.Summary.RepeatUsageSignals = cloneRepeatUsageSignals(summary.RepeatUsageSignals)
	}
	summary.WorkflowHighlights = BuildWorkflowHighlights(summary)
	ApplySummaryCaps(&summary)

	return summary, nil
}

func shareProfileRequiresRedaction(profile ShareProfile) bool {
	switch profile {
	case ShareProfilePublic,
		ShareProfileCustomerRedacted,
		ShareProfileDesignPartner,
		ShareProfileExternalRedacted,
		ShareProfileInvestorSafe:
		return true
	default:
		return false
	}
}

func normalizedSourcePrivacy(in *sourceprivacy.Contract) *sourceprivacy.Contract {
	if in == nil {
		return nil
	}
	normalized := sourceprivacy.Normalize(*in)
	return &normalized
}

func resolveDeploymentMode(in *sourceprivacy.Contract) string {
	if in == nil {
		return sourceprivacy.DeploymentModeLocalOnly
	}
	return sourceprivacy.Normalize(*in).DeploymentMode
}

func buildScanScopeSummary(snapshot state.Snapshot) *ScanScopeSummary {
	targets := snapshot.Targets
	if len(targets) == 0 && strings.TrimSpace(snapshot.Target.Mode) != "" {
		targets = []source.Target{snapshot.Target}
	}
	targetCount := len(targets)
	if targetCount == 0 {
		return nil
	}
	mode := strings.TrimSpace(snapshot.Target.Mode)
	if mode == "" && len(targets) > 0 {
		mode = strings.TrimSpace(targets[0].Mode)
	}
	scopeLabel, boundary := scanScopeLabel(mode, targetCount)
	return &ScanScopeSummary{
		Mode:           mode,
		ScopeLabel:     scopeLabel,
		SourceBoundary: boundary,
		RepoCount:      countScopeRepos(snapshot),
		TargetCount:    targetCount,
	}
}

func scanScopeLabel(mode string, targetCount int) (string, string) {
	switch strings.TrimSpace(mode) {
	case "my_setup":
		return "local machine posture", "local_machine"
	case "path":
		if targetCount > 1 {
			return "local repo group", "repo_group"
		}
		return "local repository path", "local_path"
	case "repo":
		return "remote repository", "remote_repo"
	case "org":
		return "remote organization", "remote_org"
	case source.TargetModeMulti:
		return "multi-target scan", "multi_target"
	case source.TargetModePublicSurface:
		return "public-surface assessment", "public_surface"
	default:
		return "unknown scope", "unknown"
	}
}

func countScopeRepos(snapshot state.Snapshot) int {
	set := map[string]struct{}{}
	for _, finding := range snapshot.Findings {
		if strings.TrimSpace(finding.Repo) == "" {
			continue
		}
		set[strings.TrimSpace(finding.Repo)] = struct{}{}
	}
	if len(set) == 0 && snapshot.Inventory != nil {
		for _, tool := range snapshot.Inventory.Tools {
			for _, loc := range tool.Locations {
				if strings.TrimSpace(loc.Repo) != "" {
					set[strings.TrimSpace(loc.Repo)] = struct{}{}
				}
			}
		}
	}
	return len(set)
}

func scanQualityCoverageReduced(report *scanquality.Report) bool {
	return scanquality.CoverageReduced(report)
}

func missingProofPathCount(statuses []ControlProofStatus) int {
	count := 0
	for _, status := range statuses {
		if strings.EqualFold(strings.TrimSpace(status.Status), "missing") {
			count++
		}
	}
	return count
}

func buildRuntimeEvidenceSummary(statePath string, snapshot state.Snapshot) *ingest.Summary {
	if strings.TrimSpace(statePath) == "" {
		return nil
	}
	bundle, artifactPath, err := ingest.LoadOptional(statePath)
	if err != nil {
		bundle = ingest.Bundle{}
		artifactPath = ""
	}
	sessionBundle, sessionArtifactPath, sessionErr := ingest.LoadOptionalSessionBundle(statePath)
	if sessionErr != nil {
		sessionBundle = ingest.SessionBundle{}
		sessionArtifactPath = ""
	}
	if artifactPath == "" && sessionArtifactPath == "" {
		return nil
	}
	merged := ingest.MergeRuntimeBundles(bundle, ingest.ProjectSessionsToRuntimeBundle(sessionBundle))
	summary := ingest.Correlate(snapshot, firstNonEmptyValue(artifactPath, sessionArtifactPath), merged)
	return &summary
}

func buildEvidencePacketSummary(statePath string, snapshot state.Snapshot) *ingest.EvidencePacketSummary {
	if strings.TrimSpace(statePath) == "" {
		return nil
	}
	bundle, artifactPath, err := ingest.LoadOptionalEvidencePacketBundle(statePath)
	if err != nil {
		bundle = ingest.EvidencePacketBundle{}
		artifactPath = ""
	}
	sessionBundle, sessionArtifactPath, sessionErr := ingest.LoadOptionalSessionBundle(statePath)
	if sessionErr != nil {
		sessionBundle = ingest.SessionBundle{}
		sessionArtifactPath = ""
	}
	if artifactPath == "" && sessionArtifactPath == "" {
		return nil
	}
	merged := ingest.MergeEvidencePacketBundles(bundle, ingest.ProjectSessionsToEvidencePacketBundle(sessionBundle))
	summary := ingest.CorrelateEvidencePackets(snapshot, firstNonEmptyValue(artifactPath, sessionArtifactPath), merged)
	return &summary
}

func buildSessionSummary(statePath string, snapshot state.Snapshot) *ingest.SessionSummary {
	if strings.TrimSpace(statePath) == "" {
		return nil
	}
	bundle, artifactPath, err := ingest.LoadOptionalSessionBundle(statePath)
	if err != nil || artifactPath == "" {
		return nil
	}
	summary := ingest.CorrelateSessions(snapshot, artifactPath, bundle)
	return &summary
}

func buildControlProofStatusForSummary(statePath string, snapshot state.Snapshot, riskReport *risk.Report) ([]ControlProofStatus, error) {
	chainPath := proofemit.ChainPath(state.ResolvePath(strings.TrimSpace(statePath)))
	chain, err := proofemit.LoadChain(chainPath)
	if err != nil {
		return nil, fmt.Errorf("load proof chain: %w", err)
	}

	proofSnapshot := snapshot
	if riskReport != nil {
		reportCopy := *riskReport
		proofSnapshot.RiskReport = &reportCopy
	}
	return BuildControlProofStatus(proofSnapshot, chain), nil
}

func buildDecisionTraceRefsForSummary(statePath string) (map[string][]string, error) {
	chainPath := proofemit.ChainPath(state.ResolvePath(strings.TrimSpace(statePath)))
	chain, err := proofemit.LoadChain(chainPath)
	if err != nil {
		return nil, fmt.Errorf("load proof chain: %w", err)
	}

	out := map[string][]string{}
	for _, record := range chain.Records {
		if strings.TrimSpace(record.RecordType) != "decision_trace" {
			continue
		}
		pathID, _ := record.Metadata["path_id"].(string)
		if strings.TrimSpace(pathID) == "" {
			if eventPathID, ok := record.Event["path_id"].(string); ok {
				pathID = eventPathID
			}
		}
		pathID = strings.TrimSpace(pathID)
		if pathID == "" {
			continue
		}
		traceID, _ := record.Metadata["trace_id"].(string)
		if strings.TrimSpace(traceID) == "" {
			if eventTraceID, ok := record.Event["trace_id"].(string); ok {
				traceID = eventTraceID
			}
		}
		traceID = strings.TrimSpace(traceID)
		if traceID == "" {
			traceID = strings.TrimSpace(record.RecordID)
		}
		ref := "decision_trace:" + traceID
		out[pathID] = uniqueStrings(append(out[pathID], ref))
	}
	return out, nil
}

func decorateDecisionTraceRefs(paths []risk.ActionPath, refsByPath map[string][]string) []risk.ActionPath {
	if len(paths) == 0 || len(refsByPath) == 0 {
		return paths
	}
	out := append([]risk.ActionPath(nil), paths...)
	for idx := range out {
		pathID := strings.TrimSpace(out[idx].PathID)
		if pathID == "" {
			continue
		}
		out[idx].DecisionTraceRefs = uniqueStrings(append(out[idx].DecisionTraceRefs, refsByPath[pathID]...))
	}
	return out
}

func sanitizeControlProofStatusPublic(in []ControlProofStatus, rawPaths []risk.ActionPath, sanitizedPaths []risk.ActionPath) []ControlProofStatus {
	if len(in) == 0 {
		return nil
	}

	pathIDMap := map[string]string{}
	for idx := range rawPaths {
		rawPathID := strings.TrimSpace(rawPaths[idx].PathID)
		if rawPathID == "" {
			continue
		}
		sanitizedPathID := redactValue("path", rawPathID, 8)
		if idx < len(sanitizedPaths) && strings.TrimSpace(sanitizedPaths[idx].PathID) != "" {
			sanitizedPathID = strings.TrimSpace(sanitizedPaths[idx].PathID)
		}
		pathIDMap[rawPathID] = sanitizedPathID
	}

	out := make([]ControlProofStatus, 0, len(in))
	for _, item := range in {
		copyItem := item
		linkedPathID := strings.TrimSpace(copyItem.LinkedActionPathID)
		if sanitizedPathID, ok := pathIDMap[linkedPathID]; ok {
			copyItem.LinkedActionPathID = sanitizedPathID
		} else if linkedPathID != "" {
			copyItem.LinkedActionPathID = redactValue("path", linkedPathID, 8)
		}
		out = append(out, copyItem)
	}
	return out
}

type complianceSummaryError struct {
	err error
}

func (e *complianceSummaryError) Error() string {
	if e == nil || e.err == nil {
		return ""
	}
	return e.err.Error()
}

func (e *complianceSummaryError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.err
}

func IsComplianceSummaryError(err error) bool {
	var target *complianceSummaryError
	return errors.As(err, &target)
}

func privilegeBudgetFromInventory(inv *agginventory.Inventory) agginventory.PrivilegeBudget {
	if inv == nil {
		return normalizePrivilegeBudget(agginventory.PrivilegeBudget{
			ProductionWrite: agginventory.ProductionWriteBudget{
				Configured: false,
				Status:     agginventory.ProductionTargetsStatusNotConfigured,
				Count:      nil,
			},
		})
	}
	return normalizePrivilegeBudget(inv.PrivilegeBudget)
}

func securityVisibilityFromInventory(inv *agginventory.Inventory) agginventory.SecurityVisibilitySummary {
	if inv == nil {
		return agginventory.SecurityVisibilitySummary{}
	}
	if !hasSecurityVisibilityReference(inv.SecurityVisibility) {
		return agginventory.SecurityVisibilitySummary{}
	}
	return inv.SecurityVisibility
}

func hasSecurityVisibilityReference(summary agginventory.SecurityVisibilitySummary) bool {
	return strings.TrimSpace(summary.ReferenceBasis) != ""
}

func normalizePrivilegeBudget(in agginventory.PrivilegeBudget) agginventory.PrivilegeBudget {
	status := strings.TrimSpace(in.ProductionWrite.Status)
	switch status {
	case agginventory.ProductionTargetsStatusConfigured, agginventory.ProductionTargetsStatusNotConfigured, agginventory.ProductionTargetsStatusInvalid:
		// Keep explicit status.
	default:
		if in.ProductionWrite.Configured {
			status = agginventory.ProductionTargetsStatusConfigured
		} else {
			status = agginventory.ProductionTargetsStatusNotConfigured
		}
	}
	in.ProductionWrite.Status = status
	in.ProductionWrite.Configured = status == agginventory.ProductionTargetsStatusConfigured
	if !in.ProductionWrite.Configured {
		in.ProductionWrite.Count = nil
	}
	if in.ProductionWrite.Configured && in.ProductionWrite.Count == nil {
		zero := 0
		in.ProductionWrite.Count = &zero
	}
	return in
}

func SelectTopFindings(report risk.Report, requested int) []risk.ScoredFinding {
	source := report.TopN
	if len(source) == 0 && len(report.Ranked) > 0 {
		source = report.Ranked
	}
	if requested >= 0 && requested > len(source) && len(report.Ranked) > len(source) {
		source = report.Ranked
	}
	if requested < 0 {
		return append([]risk.ScoredFinding(nil), source...)
	}
	if requested > len(source) {
		requested = len(source)
	}
	if requested < 0 {
		requested = 0
	}
	return append([]risk.ScoredFinding(nil), source[:requested]...)
}

func PublicSanitizeFindings(in []risk.ScoredFinding) []risk.ScoredFinding {
	out := make([]risk.ScoredFinding, 0, len(in))
	for _, item := range in {
		copyItem := item
		copyItem.CanonicalKey = redactValue("finding", copyItem.CanonicalKey, 12)
		copyItem.Finding.Location = redactValue("loc", copyItem.Finding.Location, 8)
		copyItem.Finding.Repo = redactValue("repo", copyItem.Finding.Repo, 6)
		copyItem.Finding.Org = redactValue("org", copyItem.Finding.Org, 6)
		out = append(out, copyItem)
	}
	return out
}

func resolveGeneratedAt(generatedAt time.Time, snapshot state.Snapshot) time.Time {
	if !generatedAt.IsZero() {
		return generatedAt.UTC().Truncate(time.Second)
	}
	if snapshot.RiskReport != nil && strings.TrimSpace(snapshot.RiskReport.GeneratedAt) != "" {
		parsed, err := time.Parse(time.RFC3339, strings.TrimSpace(snapshot.RiskReport.GeneratedAt))
		if err == nil {
			return parsed.UTC().Truncate(time.Second)
		}
	}
	return time.Now().UTC().Truncate(time.Second)
}

func buildProofReference(statePath string, top []risk.ScoredFinding) (ProofReference, error) {
	resolvedStatePath := state.ResolvePath(strings.TrimSpace(statePath))
	chainPath := proofemit.ChainPath(resolvedStatePath)
	verifyResult := verifycore.Result{}
	if verified, err := verifycore.Chain(chainPath); err == nil {
		verifyResult = verified
	}

	chain, err := proofemit.LoadChain(chainPath)
	if err != nil {
		return ProofReference{}, fmt.Errorf("load proof chain: %w", err)
	}
	byType := map[string]int{}
	for _, record := range chain.Records {
		recordType := strings.TrimSpace(record.RecordType)
		if recordType == "" {
			recordType = "unknown"
		}
		byType[recordType]++
	}
	typeCounts := make([]RecordTypeCount, 0, len(byType))
	for recordType, count := range byType {
		typeCounts = append(typeCounts, RecordTypeCount{RecordType: recordType, Count: count})
	}
	sort.Slice(typeCounts, func(i, j int) bool {
		return typeCounts[i].RecordType < typeCounts[j].RecordType
	})

	keys := make([]string, 0, len(top))
	seen := map[string]struct{}{}
	for _, item := range top {
		if strings.TrimSpace(item.CanonicalKey) == "" {
			continue
		}
		if _, exists := seen[item.CanonicalKey]; exists {
			continue
		}
		seen[item.CanonicalKey] = struct{}{}
		keys = append(keys, item.CanonicalKey)
	}
	sort.Strings(keys)

	headHash := strings.TrimSpace(verifyResult.HeadHash)
	if headHash == "" {
		headHash = strings.TrimSpace(chain.HeadHash)
	}
	recordCount := verifyResult.Count
	if recordCount == 0 {
		recordCount = len(chain.Records)
	}

	return ProofReference{
		ChainPath:            filepath.Clean(chainPath),
		HeadHash:             headHash,
		RecordCount:          recordCount,
		RecordTypeCounts:     typeCounts,
		CanonicalFindingKeys: keys,
	}, nil
}

func buildLifecycleSummary(
	m *manifest.Manifest,
	snapshotIdentities []manifest.IdentityRecord,
	transitions []lifecycle.Transition,
	gaps []lifecycle.Gap,
	inventory *agginventory.Inventory,
) LifecycleSummary {
	identities := []manifest.IdentityRecord{}
	if m != nil {
		identities = append(identities, model.FilterLegacyArtifactIdentityRecords(m.Identities)...)
	}
	if len(identities) == 0 {
		identities = append(identities, model.FilterLegacyArtifactIdentityRecords(snapshotIdentities)...)
	}

	underReview := 0
	revoked := 0
	deprecated := 0
	for _, record := range identities {
		switch strings.TrimSpace(record.Status) {
		case identity.StateUnderReview:
			underReview++
		case identity.StateRevoked:
			revoked++
		case identity.StateDeprecated:
			deprecated++
		}
	}

	normalizedTransitions := make([]LifecycleTransition, 0, len(transitions))
	for _, item := range transitions {
		normalizedTransitions = append(normalizedTransitions, LifecycleTransition{
			AgentID:       strings.TrimSpace(item.AgentID),
			PreviousState: strings.TrimSpace(item.PreviousState),
			NewState:      strings.TrimSpace(item.NewState),
			Trigger:       strings.TrimSpace(item.Trigger),
			Timestamp:     strings.TrimSpace(item.Timestamp),
		})
	}
	sort.Slice(normalizedTransitions, func(i, j int) bool {
		if normalizedTransitions[i].Timestamp != normalizedTransitions[j].Timestamp {
			return normalizedTransitions[i].Timestamp > normalizedTransitions[j].Timestamp
		}
		if normalizedTransitions[i].AgentID != normalizedTransitions[j].AgentID {
			return normalizedTransitions[i].AgentID < normalizedTransitions[j].AgentID
		}
		return normalizedTransitions[i].Trigger < normalizedTransitions[j].Trigger
	})
	if len(normalizedTransitions) > 5 {
		normalizedTransitions = normalizedTransitions[:5]
	}
	if len(gaps) == 0 {
		gaps = lifecycle.DetectGaps(lifecycle.GapInput{
			Identities:  identities,
			Inventory:   inventory,
			Transitions: transitions,
		})
	}

	return LifecycleSummary{
		IdentityCount:      len(identities),
		UnderReviewCount:   underReview,
		RevokedCount:       revoked,
		DeprecatedCount:    deprecated,
		PendingActionCount: underReview + revoked + deprecated + len(gaps),
		Gaps:               append([]lifecycle.Gap(nil), gaps...),
		Queue:              lifecycle.BuildQueue(gaps),
		RecentTransitions:  normalizedTransitions,
	}
}

func buildRegressSummary(baseline *regress.Baseline, result *regress.Result) *RegressSummary {
	if baseline == nil && result == nil {
		return nil
	}
	summary := &RegressSummary{BaselineProvided: baseline != nil}
	if result == nil {
		return summary
	}
	summary.DriftDetected = result.Drift
	summary.ReasonCount = result.ReasonCount
	summary.DriftCategoryCount = result.DriftCategoryCount
	summary.DriftCategories = append([]regress.DriftCategorySummary(nil), result.DriftCategories...)
	summary.ComparisonStatus = strings.TrimSpace(result.ComparisonStatus)
	summary.ComparisonIssues = append([]string(nil), result.ComparisonIssues...)
	byCode := map[string]int{}
	for _, reason := range result.Reasons {
		code := strings.TrimSpace(reason.Code)
		if code == "" {
			code = "unknown"
		}
		byCode[code]++
	}
	groups := make([]ReasonGroup, 0, len(byCode))
	for code, count := range byCode {
		groups = append(groups, ReasonGroup{Code: code, Count: count})
	}
	sort.Slice(groups, func(i, j int) bool {
		if groups[i].Code != groups[j].Code {
			return groups[i].Code < groups[j].Code
		}
		return groups[i].Count > groups[j].Count
	})
	summary.ReasonGroups = groups
	return summary
}

func cloneRegressSummary(in *RegressSummary) *RegressSummary {
	if in == nil {
		return nil
	}
	out := *in
	out.ReasonGroups = append([]ReasonGroup(nil), in.ReasonGroups...)
	out.ComparisonIssues = append([]string(nil), in.ComparisonIssues...)
	out.DriftCategories = append([]regress.DriftCategorySummary(nil), in.DriftCategories...)
	return &out
}

func buildDeltaSummary(snapshot state.Snapshot, previous *state.Snapshot, top int) DeltaSummary {
	riskCurrent := averageRisk(snapshot.RiskReport, top)
	riskPrevious := 0.0
	riskHasPrevious := false
	if previous != nil {
		riskPrevious = averageRisk(previous.RiskReport, top)
		riskHasPrevious = previous.RiskReport != nil
	}
	riskDelta := 0.0
	if riskHasPrevious {
		riskDelta = round2(riskCurrent - riskPrevious)
	}

	profileCurrent := 0.0
	profilePrevious := 0.0
	profileDelta := 0.0
	profileHasPrevious := false
	if snapshot.Profile != nil {
		profileCurrent = round2(snapshot.Profile.CompliancePercent)
		profileDelta = round2(snapshot.Profile.DeltaPercent)
		profilePrevious = round2(profileCurrent - profileDelta)
		profileHasPrevious = profileDelta != 0 || previous != nil
	}

	postureCurrent := 0.0
	posturePrevious := 0.0
	postureDelta := 0.0
	postureHasPrevious := false
	if snapshot.PostureScore != nil {
		postureCurrent = round2(snapshot.PostureScore.Score)
		postureDelta = round2(snapshot.PostureScore.TrendDelta)
		posturePrevious = round2(postureCurrent - postureDelta)
		postureHasPrevious = postureDelta != 0 || previous != nil
	}

	return DeltaSummary{
		RiskScoreTrend: DeltaMetric{
			Current:     riskCurrent,
			Previous:    riskPrevious,
			Delta:       riskDelta,
			HasPrevious: riskHasPrevious,
		},
		ProfileComplianceDelta: DeltaMetric{
			Current:     profileCurrent,
			Previous:    profilePrevious,
			Delta:       profileDelta,
			HasPrevious: profileHasPrevious,
		},
		PostureScoreTrend: DeltaMetric{
			Current:     postureCurrent,
			Previous:    posturePrevious,
			Delta:       postureDelta,
			HasPrevious: postureHasPrevious,
		},
	}
}

func averageRisk(report *risk.Report, top int) float64 {
	if report == nil {
		return 0
	}
	selected := SelectTopFindings(*report, top)
	if len(selected) == 0 {
		return 0
	}
	total := 0.0
	for _, item := range selected {
		total += item.Score
	}
	return round2(total / float64(len(selected)))
}

func buildHeadline(snapshot state.Snapshot) Headline {
	headline := Headline{}
	if snapshot.PostureScore != nil {
		headline.Score = round2(snapshot.PostureScore.Score)
		headline.Grade = strings.TrimSpace(snapshot.PostureScore.Grade)
	}
	if snapshot.Profile != nil {
		headline.ComplianceStatus = strings.TrimSpace(snapshot.Profile.Status)
		headline.Compliance = round2(snapshot.Profile.CompliancePercent)
	}
	return headline
}

func buildMethodology(snapshot state.Snapshot) Methodology {
	methodology := Methodology{
		WrkrVersion:         "devel",
		ScanStartedAt:       "",
		ScanCompletedAt:     "",
		ScanDurationSeconds: 0,
		RepoCount:           0,
		FileCountProcessed:  0,
		DetectorCount:       0,
		CommandSet: []string{
			"wrkr scan --json",
			"wrkr report --template public --share-profile public --json",
		},
		SampleDefinition:  "discovered AI tooling from configured scan target",
		ExclusionCriteria: []string{"no live endpoint probing", "no runtime execution telemetry", "no secret-value extraction"},
	}
	if snapshot.Inventory == nil {
		return methodology
	}
	method := snapshot.Inventory.Methodology
	if strings.TrimSpace(method.WrkrVersion) != "" {
		methodology.WrkrVersion = strings.TrimSpace(method.WrkrVersion)
	}
	methodology.ScanStartedAt = strings.TrimSpace(method.ScanStartedAt)
	methodology.ScanCompletedAt = strings.TrimSpace(method.ScanCompletedAt)
	methodology.ScanDurationSeconds = method.ScanDurationSeconds
	methodology.RepoCount = method.RepoCount
	methodology.FileCountProcessed = method.FileCountProcessed
	methodology.DetectorCount = len(method.Detectors)
	return methodology
}

func buildRiskItems(findings []risk.ScoredFinding, actionPaths []risk.ActionPath) []RiskItem {
	if len(actionPaths) > 0 {
		return buildActionPathRiskItems(actionPaths)
	}
	out := make([]RiskItem, 0, len(findings))
	for idx, finding := range findings {
		remediation := strings.TrimSpace(finding.Finding.Remediation)
		if remediation == "" {
			remediation = defaultRemediation(finding.Finding.FindingType)
		}
		out = append(out, RiskItem{
			Rank:         idx + 1,
			CanonicalKey: strings.TrimSpace(finding.CanonicalKey),
			Score:        round2(finding.Score),
			FindingType:  strings.TrimSpace(finding.Finding.FindingType),
			Severity:     strings.TrimSpace(finding.Finding.Severity),
			ToolType:     strings.TrimSpace(finding.Finding.ToolType),
			Org:          strings.TrimSpace(finding.Finding.Org),
			Repo:         strings.TrimSpace(finding.Finding.Repo),
			Location:     strings.TrimSpace(finding.Finding.Location),
			Rationale:    append([]string(nil), finding.Reasons...),
			Remediation:  remediation,
		})
	}
	return out
}

func buildActionPathRiskItems(paths []risk.ActionPath) []RiskItem {
	out := make([]RiskItem, 0, len(paths))
	for idx, path := range paths {
		path = risk.ProjectActionPath(path)
		rationale := []string{
			fmt.Sprintf("control_priority=%s", controlPriorityForPath(path)),
			fmt.Sprintf("risk_tier=%s", riskTierForPath(path)),
			fmt.Sprintf("inventory_risk=%s", inventoryRiskForPath(path)),
			fmt.Sprintf("control_state=%s", strings.TrimSpace(path.ControlState)),
			fmt.Sprintf("risk_zone=%s", strings.TrimSpace(path.RiskZone)),
			fmt.Sprintf("review_burden=%s", strings.TrimSpace(path.ReviewBurden)),
			fmt.Sprintf("confidence_lane=%s", strings.TrimSpace(path.ConfidenceLane)),
			fmt.Sprintf("recommended_action=%s", strings.TrimSpace(path.RecommendedAction)),
			fmt.Sprintf("delivery_chain_status=%s", strings.TrimSpace(path.DeliveryChainStatus)),
			fmt.Sprintf("business_state_surface=%s", strings.TrimSpace(path.BusinessStateSurface)),
		}
		if strings.TrimSpace(path.WorkflowTriggerClass) != "" {
			rationale = append(rationale, fmt.Sprintf("workflow_trigger_class=%s", strings.TrimSpace(path.WorkflowTriggerClass)))
		}
		if path.ProductionWrite {
			rationale = append(rationale, "production_write=true")
		}
		if strings.TrimSpace(path.ExecutionIdentityStatus) != "" {
			rationale = append(rationale, fmt.Sprintf("execution_identity_status=%s", strings.TrimSpace(path.ExecutionIdentityStatus)))
		}
		if strings.TrimSpace(path.OwnershipStatus) != "" {
			rationale = append(rationale, fmt.Sprintf("ownership_status=%s", strings.TrimSpace(path.OwnershipStatus)))
		}
		if path.SharedExecutionIdentity {
			rationale = append(rationale, "shared_execution_identity=true")
		}
		if path.StandingPrivilege {
			rationale = append(rationale, "standing_privilege=true")
		}
		if path.CredentialProvenance != nil {
			rationale = append(rationale, fmt.Sprintf("credential_provenance=%s", strings.TrimSpace(path.CredentialProvenance.Type)))
		}
		for _, semantic := range agginventory.NormalizeMutableEndpointSemantics(path.MutableEndpointSemantics) {
			rationale = append(rationale, fmt.Sprintf("mutable_endpoint_semantic=%s", strings.TrimSpace(semantic.Semantic)))
		}
		out = append(out, RiskItem{
			Rank:                   idx + 1,
			CanonicalKey:           strings.TrimSpace(path.PathID),
			Score:                  round2(math.Max(path.AttackPathScore, path.RiskScore)),
			FindingType:            "action_path",
			Severity:               actionPathSeverity(path),
			ToolType:               strings.TrimSpace(path.ToolType),
			Org:                    strings.TrimSpace(path.Org),
			Repo:                   strings.TrimSpace(path.Repo),
			Location:               strings.TrimSpace(path.Location),
			PathID:                 strings.TrimSpace(path.PathID),
			InventoryRisk:          inventoryRiskForPath(path),
			AttackPathScore:        round2(path.AttackPathScore),
			ControlPriority:        controlPriorityForPath(path),
			RiskTier:               riskTierForPath(path),
			ControlState:           strings.TrimSpace(path.ControlState),
			RiskZone:               strings.TrimSpace(path.RiskZone),
			ReviewBurden:           strings.TrimSpace(path.ReviewBurden),
			ConfidenceLane:         strings.TrimSpace(path.ConfidenceLane),
			CredentialAccess:       path.CredentialAccess,
			ProductionTargetStatus: strings.TrimSpace(path.ProductionTargetStatus),
			RecommendedAction:      strings.TrimSpace(path.RecommendedAction),
			WriteCapable:           path.WriteCapable,
			ProductionWrite:        path.ProductionWrite,
			Rationale:              rationale,
			Remediation:            actionPathRemediation(path),
		})
	}
	return out
}

func defaultRemediation(findingType string) string {
	switch strings.TrimSpace(findingType) {
	case "policy_violation":
		return "review and resolve violating policy rule before next scan"
	case "skill_policy_conflict":
		return "tighten skill grants and align permissions with policy"
	case "mcp_server":
		return "validate MCP trust controls and least-privilege transport settings"
	default:
		return "review finding context and apply deterministic least-privilege remediation"
	}
}

func buildNextActions(risks []RiskItem, lifecycleSummary LifecycleSummary, regressSummary *RegressSummary) []ChecklistItem {
	actions := make([]ChecklistItem, 0, 4)
	if len(risks) > 0 {
		text := fmt.Sprintf("triage highest risk finding %s (score %.2f)", risks[0].CanonicalKey, risks[0].Score)
		if risks[0].FindingType == "action_path" {
			text = fmt.Sprintf("review govern-first path %s in %s:%s (action=%s score=%.2f)", risks[0].PathID, risks[0].Repo, risks[0].Location, risks[0].RecommendedAction, risks[0].Score)
		}
		actions = append(actions, ChecklistItem{
			ID:   "action_top_risk",
			Text: text,
		})
	}
	if lifecycleSummary.PendingActionCount > 0 {
		actions = append(actions, ChecklistItem{
			ID:   "action_lifecycle",
			Text: fmt.Sprintf("review %d lifecycle records requiring approval/review/revocation action", lifecycleSummary.PendingActionCount),
		})
	}
	if regressSummary != nil && regressSummary.DriftDetected {
		actions = append(actions, ChecklistItem{
			ID:   "action_regress",
			Text: fmt.Sprintf("investigate %d regress drift reasons before promoting changes", regressSummary.ReasonCount),
		})
	}
	actions = append(actions, ChecklistItem{
		ID:   "action_verify",
		Text: "verify proof chain integrity before sharing artifacts externally",
	})
	if len(actions) > 4 {
		actions = actions[:4]
	}
	return actions
}

func buildSections(
	pack templatespkg.Pack,
	isPublic bool,
	headline Headline,
	methodology Methodology,
	risks []RiskItem,
	attackPathFacts []string,
	controlPathGraph *aggattack.ControlPathGraph,
	complianceSummary compliance.RollupSummary,
	privilegeBudget agginventory.PrivilegeBudget,
	securityVisibility agginventory.SecurityVisibilitySummary,
	deltas DeltaSummary,
	lifecycleSummary LifecycleSummary,
	regressSummary *RegressSummary,
	proof ProofReference,
	nextActions []ChecklistItem,
	sourcePrivacy *sourceprivacy.Contract,
) []Section {
	riskFacts := make([]string, 0, len(risks))
	for _, item := range risks {
		if item.FindingType == "action_path" {
			riskFacts = append(riskFacts, fmt.Sprintf(
				"#%d %.2f %s [%s] lane=%s action=%s state=%s zone=%s review=%s repo=%s location=%s",
				item.Rank,
				item.Score,
				actionPathRiskLabel(item),
				item.Severity,
				item.ConfidenceLane,
				item.RecommendedAction,
				item.ControlState,
				item.RiskZone,
				item.ReviewBurden,
				item.Repo,
				item.Location,
			))
			continue
		}
		riskFacts = append(riskFacts, fmt.Sprintf("#%d %.2f %s [%s] %s", item.Rank, item.Score, item.FindingType, item.Severity, item.Location))
	}
	riskFacts = append(riskFacts, attackPathFacts...)
	if len(riskFacts) == 0 {
		riskFacts = append(riskFacts, "no ranked findings are available in the current snapshot")
	}

	changeFacts := []string{
		formatDelta("risk score trend", deltas.RiskScoreTrend),
		formatDelta("profile compliance delta", deltas.ProfileComplianceDelta),
		formatDelta("posture score trend delta", deltas.PostureScoreTrend),
	}
	if regressSummary != nil {
		changeFacts = append(changeFacts, fmt.Sprintf("regress drift detected=%t reasons=%d", regressSummary.DriftDetected, regressSummary.ReasonCount))
		if strings.TrimSpace(regressSummary.ComparisonStatus) != "" {
			changeFacts = append(changeFacts, fmt.Sprintf("drift comparison status=%s", regressSummary.ComparisonStatus))
		}
		for _, issue := range regressSummary.ComparisonIssues {
			changeFacts = append(changeFacts, "drift comparison issue "+issue)
		}
		for _, group := range regressSummary.ReasonGroups {
			changeFacts = append(changeFacts, fmt.Sprintf("drift reason %s count=%d", group.Code, group.Count))
		}
		for _, category := range regressSummary.DriftCategories {
			changeFacts = append(changeFacts, fmt.Sprintf("drift category %s severity=%s priority=%s count=%d", category.Category, category.Severity, category.Priority, category.Count))
		}
	}

	lifecycleFacts := []string{
		fmt.Sprintf("identities=%d pending_action=%d under_review=%d revoked=%d deprecated=%d", lifecycleSummary.IdentityCount, lifecycleSummary.PendingActionCount, lifecycleSummary.UnderReviewCount, lifecycleSummary.RevokedCount, lifecycleSummary.DeprecatedCount),
	}
	for _, gap := range lifecycleSummary.Gaps {
		lifecycleFacts = append(lifecycleFacts, fmt.Sprintf("gap %s severity=%s repo=%s location=%s", gap.ReasonCode, gap.Severity, gap.Repo, gap.Location))
	}
	for _, transition := range lifecycleSummary.RecentTransitions {
		lifecycleFacts = append(lifecycleFacts, fmt.Sprintf("transition %s %s->%s (%s)", transition.AgentID, transition.PreviousState, transition.NewState, transition.Trigger))
	}

	proofFacts := []string{
		fmt.Sprintf("chain_path=%s", proof.ChainPath),
		fmt.Sprintf("head_hash=%s", proof.HeadHash),
		fmt.Sprintf("record_count=%d", proof.RecordCount),
	}
	for _, item := range proof.RecordTypeCounts {
		proofFacts = append(proofFacts, fmt.Sprintf("record_type %s=%d", item.RecordType, item.Count))
	}

	nextActionFacts := make([]string, 0, len(nextActions))
	for _, item := range nextActions {
		nextActionFacts = append(nextActionFacts, item.Text)
	}

	headlineFacts := []string{
		fmt.Sprintf("posture score %.2f (%s)", headline.Score, headline.Grade),
		fmt.Sprintf("profile status %s at %.2f%%", headline.ComplianceStatus, headline.Compliance),
		fmt.Sprintf("tools=%d write_capable=%d credential_access=%d exec_capable=%d", privilegeBudget.TotalTools, privilegeBudget.WriteCapableTools, privilegeBudget.CredentialAccessTools, privilegeBudget.ExecCapableTools),
		"bundled framework mappings stay available; profile compliance reflects only controls evidenced in the current deterministic scan state",
		"report scope stays at static posture and offline-verifiable proof; it does not claim runtime observation or control-layer enforcement",
	}
	if hasSecurityVisibilityReference(securityVisibility) {
		headlineFacts = append(headlineFacts, fmt.Sprintf("security_visibility reference=%s unknown_to_security_tools=%d unknown_to_security_agents=%d unknown_to_security_write_capable_agents=%d", securityVisibility.ReferenceBasis, securityVisibility.UnknownToSecurityTools, securityVisibility.UnknownToSecurityAgents, securityVisibility.UnknownToSecurityWriteCapableAgents))
	} else {
		headlineFacts = append(headlineFacts, "security_visibility reference_basis unavailable; unknown_to_security claims suppressed until a saved-state basis is available")
	}
	headlineFacts = append(headlineFacts, compliance.ExplainRollupSummary(complianceSummary, 3)...)
	if privilegeBudget.ProductionWrite.Configured && privilegeBudget.ProductionWrite.Count != nil {
		headlineFacts = append(headlineFacts, fmt.Sprintf("production_write=%d (status=%s)", *privilegeBudget.ProductionWrite.Count, privilegeBudget.ProductionWrite.Status))
	} else {
		headlineFacts = append(headlineFacts, fmt.Sprintf("production targets %s; default claim scope is write_capable=%d", privilegeBudget.ProductionWrite.Status, privilegeBudget.WriteCapableTools))
	}
	if sourcePrivacy != nil {
		headlineFacts = append(headlineFacts, fmt.Sprintf("source_privacy deployment_mode=%s retention=%s retained=%t raw_source_in_artifacts=%t serialized_locations=%s cleanup_status=%s",
			sourcePrivacy.DeploymentMode,
			sourcePrivacy.RetentionMode,
			sourcePrivacy.MaterializedSourceRetained,
			sourcePrivacy.RawSourceInArtifacts,
			sourcePrivacy.SerializedLocations,
			sourcePrivacy.CleanupStatus,
		))
		if len(sourcePrivacy.Warnings) > 0 {
			headlineFacts = append(headlineFacts, fmt.Sprintf("source_privacy warnings=%d details_redacted", len(sourcePrivacy.Warnings)))
		}
	}
	if controlPathGraph != nil {
		headlineFacts = append(headlineFacts, fmt.Sprintf("control_path_graph version=%s nodes=%d edges=%d",
			controlPathGraph.Version,
			controlPathGraph.Summary.TotalNodes,
			controlPathGraph.Summary.TotalEdges,
		))
		for _, item := range controlPathGraph.Summary.NodeKinds {
			headlineFacts = append(headlineFacts, fmt.Sprintf("control_path_graph nodes[%s]=%d", item.Kind, item.Count))
		}
		for _, item := range controlPathGraph.Summary.EdgeKinds {
			headlineFacts = append(headlineFacts, fmt.Sprintf("control_path_graph edges[%s]=%d", item.Kind, item.Count))
		}
	}

	methodologyFacts := []string{
		fmt.Sprintf("wrkr_version=%s", methodology.WrkrVersion),
		fmt.Sprintf("scan_window=%s to %s duration=%.2fs", methodology.ScanStartedAt, methodology.ScanCompletedAt, methodology.ScanDurationSeconds),
		fmt.Sprintf("repo_count=%d file_count_processed=%d detector_count=%d", methodology.RepoCount, methodology.FileCountProcessed, methodology.DetectorCount),
		fmt.Sprintf("sample_definition=%s", methodology.SampleDefinition),
	}
	for _, command := range methodology.CommandSet {
		methodologyFacts = append(methodologyFacts, fmt.Sprintf("command=%s", command))
	}
	for _, exclusion := range methodology.ExclusionCriteria {
		methodologyFacts = append(methodologyFacts, fmt.Sprintf("exclusion=%s", exclusion))
	}

	sections := []Section{
		{
			ID:     SectionHeadline,
			Title:  pack.HeadlineTitle,
			Facts:  headlineFacts,
			Impact: postureImpact(headline),
			Action: postureAction(headline),
			Proof:  proof,
		},
		{
			ID:     SectionTopRisks,
			Title:  pack.TopRisksTitle,
			Facts:  riskFacts,
			Impact: riskImpact(risks),
			Action: riskAction(risks),
			Proof:  proof,
		},
		{
			ID:     SectionChanges,
			Title:  pack.ChangesTitle,
			Facts:  changeFacts,
			Impact: changeImpact(deltas, regressSummary),
			Action: changeAction(deltas, regressSummary),
			Proof:  proof,
		},
		{
			ID:     SectionLifecycle,
			Title:  pack.LifecycleTitle,
			Facts:  lifecycleFacts,
			Impact: lifecycleImpact(lifecycleSummary),
			Action: lifecycleAction(lifecycleSummary),
			Proof:  proof,
		},
		{
			ID:     SectionProof,
			Title:  pack.ProofTitle,
			Facts:  proofFacts,
			Impact: "proof chain references are attached for deterministic traceability",
			Action: "preserve chain path and head hash when distributing this artifact",
			Proof:  proof,
		},
		{
			ID:     SectionNextAction,
			Title:  pack.ActionsTitle,
			Facts:  nextActionFacts,
			Impact: "deterministic next actions focus operators on highest leverage controls",
			Action: "execute checklist items in order and rescan to confirm posture improvement",
			Proof:  proof,
		},
	}
	if isPublic {
		publicSections := []Section{
			sections[0],
			{
				ID:     SectionMethodology,
				Title:  "Methodology",
				Facts:  methodologyFacts,
				Impact: "transparent, reproducible method metadata improves external credibility",
				Action: "publish command set and exclusion criteria alongside headline findings",
				Proof:  proof,
			},
		}
		publicSections = append(publicSections, sections[1:]...)
		return publicSections
	}
	return sections
}

func formatDelta(label string, metric DeltaMetric) string {
	if !metric.HasPrevious {
		return fmt.Sprintf("%s current=%.2f delta=0.00 (no previous reference)", label, metric.Current)
	}
	return fmt.Sprintf("%s current=%.2f previous=%.2f delta=%+.2f", label, metric.Current, metric.Previous, metric.Delta)
}

func postureImpact(headline Headline) string {
	if strings.EqualFold(headline.ComplianceStatus, "fail") {
		return "profile compliance is failing and introduces immediate governance risk"
	}
	if headline.Score < 70 {
		return "posture score is below the safe operating threshold"
	}
	return "posture score and compliance status indicate controlled operational risk"
}

func postureAction(headline Headline) string {
	if strings.EqualFold(headline.ComplianceStatus, "fail") {
		return "resolve failing or missing controls, regenerate evidence, and rerun scan with the same deterministic inputs"
	}
	return "monitor score trend and keep profile compliance above configured minimums"
}

func riskImpact(risks []RiskItem) string {
	if len(risks) == 0 {
		return "no prioritized risks were emitted by the current state snapshot"
	}
	return fmt.Sprintf("top %d risks concentrate the highest blast-radius findings", len(risks))
}

func riskAction(risks []RiskItem) string {
	if len(risks) == 0 {
		return "maintain current controls and keep deterministic scans on schedule"
	}
	return "work highest score first and apply deterministic least-privilege remediation"
}

func changeImpact(deltas DeltaSummary, regressSummary *RegressSummary) string {
	if regressSummary != nil && regressSummary.DriftDetected {
		return "regress drift is present and indicates contract movement since baseline"
	}
	if deltas.PostureScoreTrend.HasPrevious && deltas.PostureScoreTrend.Delta < 0 {
		return "posture trend declined relative to previous state"
	}
	return "change deltas remain within expected deterministic variance"
}

func changeAction(deltas DeltaSummary, regressSummary *RegressSummary) string {
	if regressSummary != nil && regressSummary.DriftDetected {
		return "triage grouped regress reasons and block promotion until drift is understood"
	}
	if deltas.PostureScoreTrend.HasPrevious && deltas.PostureScoreTrend.Delta < 0 {
		return "investigate posture regression causes and remediate before next release gate"
	}
	return "continue baseline comparison on every governance scan cadence"
}

func lifecycleImpact(summary LifecycleSummary) string {
	if summary.PendingActionCount == 0 {
		return "no lifecycle identities are waiting on governance action"
	}
	return fmt.Sprintf("%d identities require lifecycle approval/review/revocation handling", summary.PendingActionCount)
}

func lifecycleAction(summary LifecycleSummary) string {
	if summary.PendingActionCount == 0 {
		return "maintain periodic lifecycle review cadence"
	}
	return "prioritize under_review and revoked identities before enabling additional autonomy"
}

func round2(value float64) float64 {
	return math.Round(value*100) / 100
}

func buildAttackPathSummary(report risk.Report) AttackPathSummary {
	out := AttackPathSummary{Total: len(report.AttackPaths), TopPathIDs: []string{}}
	for _, path := range report.TopAttackPaths {
		out.TopPathIDs = append(out.TopPathIDs, strings.TrimSpace(path.PathID))
	}
	out.TopPathIDs = uniqueStrings(out.TopPathIDs)
	return out
}

func buildAttackPathFacts(report risk.Report) []string {
	if len(report.TopAttackPaths) == 0 {
		return []string{"attack paths: none generated from current findings"}
	}
	facts := make([]string, 0, len(report.TopAttackPaths)+1)
	facts = append(facts, fmt.Sprintf("attack paths total=%d", len(report.AttackPaths)))
	for idx, path := range report.TopAttackPaths {
		facts = append(facts, fmt.Sprintf("attack_path #%d score=%.2f id=%s", idx+1, path.PathScore, path.PathID))
	}
	return facts
}

func buildAssessmentSummary(paths []risk.ActionPath, controlFirst *risk.ActionPathToControlFirst, inventory *agginventory.Inventory, proof ProofReference) *AssessmentSummary {
	if len(paths) == 0 {
		return nil
	}
	identityToReviewFirst, identityToRevokeFirst := risk.BuildIdentityActionTargets(paths)
	summary := &AssessmentSummary{
		GovernablePathCount:       len(paths),
		WriteCapablePathCount:     0,
		ProductionBackedPathCount: 0,
		OwnerlessExposure:         risk.BuildOwnerlessExposure(paths),
		IdentityExposureSummary:   risk.BuildIdentityExposureSummary(paths, inventory),
		IdentityToReviewFirst:     identityToReviewFirst,
		IdentityToRevokeFirst:     identityToRevokeFirst,
		ProofChainPath:            proof.ChainPath,
	}
	for _, path := range paths {
		if path.WriteCapable {
			summary.WriteCapablePathCount++
		}
		if path.ProductionWrite {
			summary.ProductionBackedPathCount++
		}
		if summary.TopExecutionIdentityBacked == nil && strings.TrimSpace(path.ExecutionIdentityStatus) == "known" {
			candidate := path
			summary.TopExecutionIdentityBacked = &candidate
		}
	}
	if controlFirst != nil {
		candidate := controlFirst.Path
		summary.TopPathToControlFirst = &candidate
	}
	return summary
}

func decorateControlBacklogFromActionPaths(backlog *controlbacklog.Backlog, paths []risk.ActionPath) *controlbacklog.Backlog {
	if backlog == nil {
		return nil
	}
	copyBacklog := *backlog
	copyBacklog.Items = append([]controlbacklog.Item(nil), backlog.Items...)
	if len(copyBacklog.Items) == 0 || len(paths) == 0 {
		return &copyBacklog
	}

	byPathID := map[string]risk.ActionPath{}
	byRepoPath := map[string]risk.ActionPath{}
	for _, rawPath := range paths {
		path := risk.ProjectActionPath(rawPath)
		if pathID := strings.TrimSpace(path.PathID); pathID != "" {
			byPathID[pathID] = path
		}
		if key := strings.TrimSpace(path.Repo) + "|" + strings.TrimSpace(path.Location); key != "|" {
			byRepoPath[key] = path
		}
	}

	for idx := range copyBacklog.Items {
		item := copyBacklog.Items[idx]
		path, ok := byPathID[strings.TrimSpace(item.LinkedActionPathID)]
		if !ok {
			path, ok = byRepoPath[strings.TrimSpace(item.Repo)+"|"+strings.TrimSpace(item.Path)]
		}
		if !ok {
			continue
		}
		copyBacklog.Items[idx].ControlState = firstNonEmptyValue(strings.TrimSpace(path.ControlState), strings.TrimSpace(item.ControlState))
		copyBacklog.Items[idx].ControlStateReasons = uniqueStrings(append(append([]string(nil), item.ControlStateReasons...), path.ControlStateReasons...))
		copyBacklog.Items[idx].RiskZone = firstNonEmptyValue(strings.TrimSpace(path.RiskZone), strings.TrimSpace(item.RiskZone))
		copyBacklog.Items[idx].RiskZoneReasons = uniqueStrings(append(append([]string(nil), item.RiskZoneReasons...), path.RiskZoneReasons...))
		copyBacklog.Items[idx].ReviewBurden = firstNonEmptyValue(strings.TrimSpace(path.ReviewBurden), strings.TrimSpace(item.ReviewBurden))
		copyBacklog.Items[idx].ReviewBurdenReasons = uniqueStrings(append(append([]string(nil), item.ReviewBurdenReasons...), path.ReviewBurdenReasons...))
		copyBacklog.Items[idx].ConfidenceLane = firstNonEmptyValue(strings.TrimSpace(path.ConfidenceLane), strings.TrimSpace(item.ConfidenceLane))
		copyBacklog.Items[idx].ConfidenceLaneReasons = uniqueStrings(append(append([]string(nil), item.ConfidenceLaneReasons...), path.ConfidenceLaneReasons...))
		copyBacklog.Items[idx].AutonomyTier = firstNonEmptyValue(strings.TrimSpace(path.AutonomyTier), strings.TrimSpace(item.AutonomyTier))
		copyBacklog.Items[idx].AutonomyTierReasons = uniqueStrings(append(append([]string(nil), item.AutonomyTierReasons...), path.AutonomyTierReasons...))
		copyBacklog.Items[idx].AutonomyTierEvidenceRefs = uniqueStrings(append(append([]string(nil), item.AutonomyTierEvidenceRefs...), path.AutonomyTierEvidenceRefs...))
		copyBacklog.Items[idx].DelegationReadinessState = firstNonEmptyValue(strings.TrimSpace(path.DelegationReadinessState), strings.TrimSpace(item.DelegationReadinessState))
		copyBacklog.Items[idx].DelegationReadinessReasons = uniqueStrings(append(append([]string(nil), item.DelegationReadinessReasons...), path.DelegationReadinessReasons...))
		copyBacklog.Items[idx].RecommendedControl = firstNonEmptyValue(strings.TrimSpace(path.RecommendedControl), strings.TrimSpace(item.RecommendedControl))
		copyBacklog.Items[idx].RecommendedControlReasons = uniqueStrings(append(append([]string(nil), item.RecommendedControlReasons...), path.RecommendedControlReasons...))
		copyBacklog.Items[idx].RiskClassificationValidationReasons = uniqueStrings(append(append([]string(nil), item.RiskClassificationValidationReasons...), path.RiskClassificationValidationReasons...))
		copyBacklog.Items[idx].RiskClassificationValidationRefs = uniqueStrings(append(append([]string(nil), item.RiskClassificationValidationRefs...), path.RiskClassificationValidationRefs...))
		copyBacklog.Items[idx].RecommendedActionContract = risk.CloneRecommendedActionContract(path.RecommendedActionContract)
		copyBacklog.Items[idx].TodayPath = risk.CloneGovernedPathView(path.TodayPath)
		copyBacklog.Items[idx].RecommendedGovernedPath = risk.CloneGovernedPathView(path.RecommendedGovernedPath)
		copyBacklog.Items[idx].DecisionPrecedent = risk.CloneDecisionPrecedent(path.DecisionPrecedent)
		copyBacklog.Items[idx].PolicyCoverageStatus = firstNonEmptyValue(strings.TrimSpace(path.PolicyCoverageStatus), strings.TrimSpace(item.PolicyCoverageStatus))
		copyBacklog.Items[idx].PolicyRefs = uniqueStrings(append(append([]string(nil), item.PolicyRefs...), path.PolicyRefs...))
		copyBacklog.Items[idx].PolicyMissingReasons = uniqueStrings(append(append([]string(nil), item.PolicyMissingReasons...), path.PolicyMissingReasons...))
		copyBacklog.Items[idx].PolicyEvidenceRefs = uniqueStrings(append(append([]string(nil), item.PolicyEvidenceRefs...), path.PolicyEvidenceRefs...))
		copyBacklog.Items[idx].PolicyConfidence = firstNonEmptyValue(strings.TrimSpace(path.PolicyConfidence), strings.TrimSpace(item.PolicyConfidence))
		copyBacklog.Items[idx].ClosureRequirements = risk.CloneClosureRequirements(path.ClosureRequirements)
		copyBacklog.Items[idx].EvidenceCompleteness = risk.CloneEvidenceCompleteness(path.EvidenceCompleteness)
		copyBacklog.Items[idx].ClosureCriteria = risk.ClosureCriteriaText(copyBacklog.Items[idx].ClosureRequirements, strings.TrimSpace(item.ClosureCriteria))
		copyBacklog.Items[idx].Queue = queueForActionPath(path)
		copyBacklog.Items[idx].FindingVisibility = visibilityForQueue(copyBacklog.Items[idx].Queue)
		copyBacklog.Items[idx].Remediation = risk.RemediationForActionPath(path)
		if copyBacklog.Items[idx].CredentialProvenance == nil {
			copyBacklog.Items[idx].CredentialProvenance = agginventory.CloneCredentialProvenance(path.CredentialProvenance)
		}
		if copyBacklog.Items[idx].TrustDepth == nil {
			copyBacklog.Items[idx].TrustDepth = agginventory.CloneTrustDepth(path.TrustDepth)
		}
	}
	return &copyBacklog
}

func decorateControlBacklogDecisionPrecedents(backlog *controlbacklog.Backlog, paths []risk.ActionPath) *controlbacklog.Backlog {
	if backlog == nil || len(backlog.Items) == 0 || len(paths) == 0 {
		return backlog
	}
	copyBacklog := *backlog
	copyBacklog.Items = append([]controlbacklog.Item(nil), backlog.Items...)
	byPathID := map[string]risk.ActionPath{}
	byRepoPath := map[string]risk.ActionPath{}
	for _, rawPath := range paths {
		path := risk.ProjectActionPath(rawPath)
		if pathID := strings.TrimSpace(path.PathID); pathID != "" {
			byPathID[pathID] = path
		}
		if key := strings.TrimSpace(path.Repo) + "|" + strings.TrimSpace(path.Location); key != "|" {
			byRepoPath[key] = path
		}
	}
	for idx := range copyBacklog.Items {
		item := copyBacklog.Items[idx]
		path, ok := byPathID[strings.TrimSpace(item.LinkedActionPathID)]
		if !ok {
			path, ok = byRepoPath[strings.TrimSpace(item.Repo)+"|"+strings.TrimSpace(item.Path)]
		}
		if !ok {
			continue
		}
		copyBacklog.Items[idx].DecisionPrecedent = risk.CloneDecisionPrecedent(path.DecisionPrecedent)
	}
	return &copyBacklog
}

func mergeBacklogGovernance(base, overlay *controlbacklog.Backlog) *controlbacklog.Backlog {
	if base == nil {
		return overlay
	}
	if overlay == nil {
		return base
	}
	copyBacklog := *base
	copyBacklog.Items = append([]controlbacklog.Item(nil), base.Items...)
	overlayByID := map[string]controlbacklog.Item{}
	overlayByRepoPath := map[string]controlbacklog.Item{}
	seenOverlayIDs := map[string]struct{}{}
	seenOverlayRepoPaths := map[string]struct{}{}
	for _, item := range overlay.Items {
		if id := strings.TrimSpace(item.ID); id != "" {
			overlayByID[id] = item
		}
		if key := strings.TrimSpace(item.Repo) + "|" + strings.TrimSpace(item.Path); key != "|" {
			overlayByRepoPath[key] = item
		}
	}
	for idx := range copyBacklog.Items {
		current := copyBacklog.Items[idx]
		overlayItem, ok := overlayByID[strings.TrimSpace(current.ID)]
		if !ok {
			overlayItem, ok = overlayByRepoPath[strings.TrimSpace(current.Repo)+"|"+strings.TrimSpace(current.Path)]
		}
		if !ok {
			continue
		}
		if id := strings.TrimSpace(overlayItem.ID); id != "" {
			seenOverlayIDs[id] = struct{}{}
		}
		if key := strings.TrimSpace(overlayItem.Repo) + "|" + strings.TrimSpace(overlayItem.Path); key != "|" {
			seenOverlayRepoPaths[key] = struct{}{}
		}
		current.GovernanceDisposition = overlayItem.GovernanceDisposition
		current.LifecycleQueue = overlayItem.LifecycleQueue
		if overlayItem.GovernanceDisposition != nil || overlayItem.LifecycleQueue != nil {
			current.Queue = firstNonEmptyValue(strings.TrimSpace(overlayItem.Queue), strings.TrimSpace(current.Queue))
			current.FindingVisibility = firstNonEmptyValue(strings.TrimSpace(overlayItem.FindingVisibility), strings.TrimSpace(current.FindingVisibility))
			current.SecurityVisibility = firstNonEmptyValue(strings.TrimSpace(overlayItem.SecurityVisibility), strings.TrimSpace(current.SecurityVisibility))
			current.Remediation = firstNonEmptyValue(strings.TrimSpace(overlayItem.Remediation), strings.TrimSpace(current.Remediation))
			current.EvidenceGaps = uniqueStrings(append(append([]string(nil), current.EvidenceGaps...), overlayItem.EvidenceGaps...))
		}
		copyBacklog.Items[idx] = current
	}
	for _, item := range overlay.Items {
		if id := strings.TrimSpace(item.ID); id != "" {
			if _, ok := seenOverlayIDs[id]; ok {
				continue
			}
		}
		if key := strings.TrimSpace(item.Repo) + "|" + strings.TrimSpace(item.Path); key != "|" {
			if _, ok := seenOverlayRepoPaths[key]; ok {
				continue
			}
		}
		copyBacklog.Items = append(copyBacklog.Items, item)
	}
	copyBacklog.Summary = controlbacklog.SummarizeItems(copyBacklog.Items)
	return &copyBacklog
}

func actionPathSeverity(path risk.ActionPath) string {
	path = risk.ProjectActionPath(path)
	switch strings.TrimSpace(path.RiskTier) {
	case risk.RiskTierCritical:
		return model.SeverityCritical
	case risk.RiskTierHigh:
		return model.SeverityHigh
	case risk.RiskTierMedium:
		return model.SeverityMedium
	default:
		switch riskTierForPath(path) {
		case risk.RiskTierCritical:
			return model.SeverityCritical
		case risk.RiskTierHigh:
			return model.SeverityHigh
		case risk.RiskTierMedium:
			return model.SeverityMedium
		default:
			return model.SeverityLow
		}
	}
}

func actionPathRiskLabel(item RiskItem) string {
	switch strings.TrimSpace(item.ConfidenceLane) {
	case risk.ConfidenceLaneSemanticReviewCandidate:
		return "review_candidate"
	case risk.ConfidenceLaneContextOnly:
		return "context_only_evidence"
	case risk.ConfidenceLaneLikelyActionPath:
		return "likely_action_path"
	default:
		return "confirmed_action_path"
	}
}

func actionPathRemediation(path risk.ActionPath) string {
	return risk.RemediationForActionPath(path)
}

func controlPriorityForPath(path risk.ActionPath) string {
	path = risk.ProjectActionPath(path)
	if strings.TrimSpace(path.ControlPriority) != "" {
		return strings.TrimSpace(path.ControlPriority)
	}
	switch strings.TrimSpace(path.RecommendedAction) {
	case "control":
		return risk.ControlPriorityControlFirst
	case "inventory":
		return risk.ControlPriorityInventoryHygiene
	default:
		return risk.ControlPriorityReviewQueue
	}
}

func riskTierForPath(path risk.ActionPath) string {
	path = risk.ProjectActionPath(path)
	if strings.TrimSpace(path.RiskTier) != "" {
		return strings.TrimSpace(path.RiskTier)
	}
	switch strings.TrimSpace(path.RecommendedAction) {
	case "control":
		if path.ProductionWrite {
			return risk.RiskTierCritical
		}
		return risk.RiskTierHigh
	case "approval", "proof":
		return risk.RiskTierMedium
	default:
		return risk.RiskTierLow
	}
}

func inventoryRiskForPath(path risk.ActionPath) string {
	path = risk.ProjectActionPath(path)
	if strings.TrimSpace(path.InventoryRisk) != "" {
		return strings.TrimSpace(path.InventoryRisk)
	}
	switch {
	case path.ProductionWrite || len(path.MatchedProductionTargets) > 0:
		return risk.InventoryRiskProductionBacked
	case path.WriteCapable || path.PullRequestWrite || path.MergeExecute || path.DeployWrite:
		return risk.InventoryRiskWriteCapable
	case path.CredentialAccess:
		return risk.InventoryRiskCredentialAccess
	default:
		return risk.InventoryRiskVisibilityOnly
	}
}

func buildComplianceSummary(statePath string, findings []source.Finding) (compliance.RollupSummary, error) {
	chainPath := proofemit.ChainPath(state.ResolvePath(strings.TrimSpace(statePath)))
	chain, err := proofemit.LoadChain(chainPath)
	if err != nil {
		return compliance.RollupSummary{}, err
	}
	summary, err := compliance.BuildRollupSummary(findings, chain)
	if err != nil {
		return compliance.RollupSummary{}, &complianceSummaryError{err: err}
	}
	return summary, nil
}

func uniqueStrings(in []string) []string {
	set := map[string]struct{}{}
	for _, item := range in {
		trimmed := strings.TrimSpace(item)
		if trimmed == "" {
			continue
		}
		set[trimmed] = struct{}{}
	}
	out := make([]string, 0, len(set))
	for item := range set {
		out = append(out, item)
	}
	sort.Strings(out)
	return out
}

func sanitizeProofReferencePublic(in ProofReference) ProofReference {
	copyRef := in
	copyRef.ChainPath = "redacted://proof-chain.json"
	keys := make([]string, 0, len(copyRef.CanonicalFindingKeys))
	for _, key := range copyRef.CanonicalFindingKeys {
		redacted := redactValue("finding", key, 12)
		if redacted == "" {
			continue
		}
		keys = append(keys, redacted)
	}
	sort.Strings(keys)
	copyRef.CanonicalFindingKeys = keys
	return copyRef
}

func sanitizeLifecycleSummaryPublic(in LifecycleSummary) LifecycleSummary {
	copySummary := in
	for idx := range copySummary.RecentTransitions {
		copySummary.RecentTransitions[idx].AgentID = redactValue("agent", copySummary.RecentTransitions[idx].AgentID, 8)
	}
	for idx := range copySummary.Gaps {
		copySummary.Gaps[idx].AgentID = redactValue("agent", copySummary.Gaps[idx].AgentID, 8)
		copySummary.Gaps[idx].Repo = redactValue("repo", copySummary.Gaps[idx].Repo, 6)
		copySummary.Gaps[idx].Location = redactValue("loc", copySummary.Gaps[idx].Location, 8)
		copySummary.Gaps[idx].Owner = redactValue("owner", copySummary.Gaps[idx].Owner, 8)
	}
	for idx := range copySummary.Queue {
		copySummary.Queue[idx].AgentID = redactValue("agent", copySummary.Queue[idx].AgentID, 8)
		copySummary.Queue[idx].Repo = redactValue("repo", copySummary.Queue[idx].Repo, 6)
		copySummary.Queue[idx].Path = redactValue("loc", copySummary.Queue[idx].Path, 8)
		copySummary.Queue[idx].Owner = redactValue("owner", copySummary.Queue[idx].Owner, 8)
		copySummary.Queue[idx].EvidenceRefs = redactStringSlice(copySummary.Queue[idx].EvidenceRefs, "evidence")
		copySummary.Queue[idx].SourceConflicts = redactStringSlice(copySummary.Queue[idx].SourceConflicts, "owner")
	}
	return copySummary
}

func sanitizeRiskItemsPublic(in []RiskItem) []RiskItem {
	out := make([]RiskItem, 0, len(in))
	for _, item := range in {
		copyItem := item
		copyItem.CanonicalKey = redactValue("finding", copyItem.CanonicalKey, 12)
		copyItem.Location = redactValue("loc", copyItem.Location, 8)
		copyItem.Repo = redactValue("repo", copyItem.Repo, 6)
		copyItem.Org = redactValue("org", copyItem.Org, 6)
		copyItem.PathID = redactValue("path", copyItem.PathID, 8)
		out = append(out, copyItem)
	}
	return out
}

func sanitizeActivationSummaryPublic(in *ActivationSummary) *ActivationSummary {
	if in == nil {
		return nil
	}
	copySummary := *in
	copySummary.Items = append([]ActivationItem(nil), in.Items...)
	for idx := range copySummary.Items {
		copySummary.Items[idx].Location = redactValue("loc", copySummary.Items[idx].Location, 8)
		copySummary.Items[idx].Repo = redactValue("repo", copySummary.Items[idx].Repo, 6)
	}
	return &copySummary
}

func sanitizeActionPathsPublic(in []risk.ActionPath) []risk.ActionPath {
	if len(in) == 0 {
		return nil
	}
	out := make([]risk.ActionPath, 0, len(in))
	for _, item := range in {
		copyItem := item
		copyItem.PathID = redactValue("path", copyItem.PathID, 8)
		copyItem.Org = redactValue("org", copyItem.Org, 6)
		copyItem.Repo = redactValue("repo", copyItem.Repo, 6)
		copyItem.AgentID = redactValue("agent", copyItem.AgentID, 8)
		copyItem.ToolFamilyID = redactValue("family", copyItem.ToolFamilyID, 8)
		copyItem.ToolInstanceID = redactValue("instance", copyItem.ToolInstanceID, 8)
		copyItem.Location = redactValue("loc", copyItem.Location, 8)
		copyItem.OperationalOwner = redactValue("owner", copyItem.OperationalOwner, 8)
		copyItem.ExecutionIdentity = redactValue("identity", copyItem.ExecutionIdentity, 8)
		copyItem.ConfigSource = redactValue("loc", copyItem.ConfigSource, 8)
		copyItem.OccurrenceRefs = redactStringSlice(copyItem.OccurrenceRefs, "path")
		copyItem.AttackPathRefs = redactStringSlice(copyItem.AttackPathRefs, "attack")
		copyItem.SourceFindingKeys = redactStringSlice(copyItem.SourceFindingKeys, "finding")
		if copyItem.CredentialProvenance != nil {
			copyItem.CredentialProvenance = agginventory.CloneCredentialProvenance(copyItem.CredentialProvenance)
			copyItem.CredentialProvenance.Subject = redactValue("credential", copyItem.CredentialProvenance.Subject, 8)
			copyItem.CredentialProvenance.EvidenceBasis = redactStringSlice(copyItem.CredentialProvenance.EvidenceBasis, "evidence")
			copyItem.CredentialProvenance.EvidenceLocation = redactValue("loc", copyItem.CredentialProvenance.EvidenceLocation, 8)
		}
		if copyItem.CredentialAuthority != nil {
			copyItem.CredentialAuthority = agginventory.CloneCredentialAuthority(copyItem.CredentialAuthority)
			copyItem.CredentialAuthority.ReasonCodes = redactStringSlice(copyItem.CredentialAuthority.ReasonCodes, "evidence")
		}
		copyItem.MutableEndpointSemantics = sanitizeMutableEndpointSemanticsPublic(copyItem.MutableEndpointSemantics)
		copyItem.Credentials = redactCredentialsPublic(copyItem.Credentials)
		copyItem.ClosureRequirements = sanitizeClosureRequirementsPublic(copyItem.ClosureRequirements)
		copyItem.EvidenceCompleteness = risk.CloneEvidenceCompleteness(copyItem.EvidenceCompleteness)
		copyItem.ActionLineage = sanitizeActionLineagePublic(copyItem.ActionLineage)
		copyItem.IntroducedBy = sanitizeIntroducedByPublic(copyItem.IntroducedBy)
		copyItem.AgenticDeliverySystemChange = sanitizeAgenticDeliverySystemChangePublic(copyItem.AgenticDeliverySystemChange)
		copyItem.RuntimeProvider = redactValue("provider", copyItem.RuntimeProvider, 8)
		copyItem.RuntimeHost = redactValue("provider", copyItem.RuntimeHost, 8)
		copyItem.RuntimeKind = redactValue("provider", copyItem.RuntimeKind, 8)
		copyItem.ModelProvider = redactValue("provider", copyItem.ModelProvider, 8)
		copyItem.ModelVersion = redactValue("provider", copyItem.ModelVersion, 8)
		copyItem.ExecutionEnvironment = redactValue("provider", copyItem.ExecutionEnvironment, 8)
		copyItem.StateLocationRefs = redactStringSlice(copyItem.StateLocationRefs, "state")
		copyItem.StateDigestRefs = redactStringSlice(copyItem.StateDigestRefs, "digest")
		copyItem.AgentIdentity = sanitizeAgentIdentityPublic(copyItem.AgentIdentity)
		copyItem.DecisionPrecedent = sanitizeDecisionPrecedentPublic(copyItem.DecisionPrecedent)
		copyItem.DeliveryControlContext = sanitizeDeliveryControlContextPublic(copyItem.DeliveryControlContext)
		copyItem.EvidencePacketRefs = redactStringSlice(copyItem.EvidencePacketRefs, "packet")
		copyItem.DecisionTraceRefs = redactStringSlice(copyItem.DecisionTraceRefs, "proof")
		targets := make([]string, 0, len(copyItem.MatchedProductionTargets))
		for _, target := range copyItem.MatchedProductionTargets {
			redacted := redactValue("target", target, 8)
			if redacted == "" {
				continue
			}
			targets = append(targets, redacted)
		}
		copyItem.MatchedProductionTargets = targets
		out = append(out, copyItem)
	}
	return out
}

func sanitizeActionPathToControlFirstPublic(in *risk.ActionPathToControlFirst) *risk.ActionPathToControlFirst {
	if in == nil {
		return nil
	}
	copySummary := in.Summary
	paths := sanitizeActionPathsPublic([]risk.ActionPath{in.Path})
	if len(paths) == 0 {
		return &risk.ActionPathToControlFirst{Summary: copySummary}
	}
	return &risk.ActionPathToControlFirst{
		Summary: copySummary,
		Path:    paths[0],
	}
}

func sanitizeWorkflowChainsPublic(in *agentresolver.WorkflowChainArtifact) *agentresolver.WorkflowChainArtifact {
	if in == nil {
		return nil
	}
	out := &agentresolver.WorkflowChainArtifact{
		Version: strings.TrimSpace(in.Version),
		Summary: sanitizeWorkflowChainSummaryPublic(in.Summary),
		Chains:  make([]agentresolver.WorkflowChain, 0, len(in.Chains)),
	}
	for _, chain := range in.Chains {
		copyChain := chain
		copyChain.PathIDs = redactStringSlice(copyChain.PathIDs, "path")
		copyChain.GraphNodeRefs = redactStringSlice(copyChain.GraphNodeRefs, "node")
		copyChain.GraphEdgeRefs = redactStringSlice(copyChain.GraphEdgeRefs, "edge")
		copyChain.ProofRefs = redactStringSlice(copyChain.ProofRefs, "proof")
		copyChain.EvidenceRefs = redactStringSlice(copyChain.EvidenceRefs, "evidence")
		copyChain.SourceFindingKeys = redactStringSlice(copyChain.SourceFindingKeys, "finding")
		copyChain.Repo = sanitizeWorkflowChainDimensionPublic(copyChain.Repo, "repo")
		copyChain.PullRequest = sanitizeWorkflowChainDimensionPublic(copyChain.PullRequest, "pr")
		copyChain.Workflow = sanitizeWorkflowChainDimensionPublic(copyChain.Workflow, "workflow")
		copyChain.Task = sanitizeWorkflowChainDimensionPublic(copyChain.Task, "task")
		copyChain.Tool = sanitizeWorkflowChainDimensionPublic(copyChain.Tool, "tool")
		copyChain.Credential = sanitizeWorkflowChainDimensionPublic(copyChain.Credential, "credential")
		copyChain.Owner = sanitizeWorkflowChainDimensionPublic(copyChain.Owner, "owner")
		copyChain.Approval = sanitizeWorkflowChainDimensionPublic(copyChain.Approval, "approval")
		copyChain.Target = sanitizeWorkflowChainDimensionPublic(copyChain.Target, "target")
		copyChain.Evidence = sanitizeWorkflowChainDimensionPublic(copyChain.Evidence, "evidence")
		copyChain.Outcome = sanitizeWorkflowChainDimensionPublic(copyChain.Outcome, "outcome")
		copyChain.IntroducedBy = sanitizeIntroducedByPublic(copyChain.IntroducedBy)
		out.Chains = append(out.Chains, copyChain)
	}
	return out
}

func sanitizeWorkflowChainDimensionPublic(in agentresolver.WorkflowChainDimension, prefix string) agentresolver.WorkflowChainDimension {
	out := in
	out.Key = redactValue(prefix, out.Key, 8)
	out.Label = redactValue(prefix, out.Label, 8)
	out.EvidenceRefs = redactStringSlice(out.EvidenceRefs, "evidence")
	return out
}

func sanitizeWorkflowChainSummaryPublic(in agentresolver.WorkflowChainSummary) agentresolver.WorkflowChainSummary {
	out := in
	out.Repos = sanitizeWorkflowChainRollupsPublic(in.Repos, "repo")
	out.Workflows = sanitizeWorkflowChainRollupsPublic(in.Workflows, "workflow")
	return out
}

func sanitizeWorkflowChainRollupsPublic(in []agentresolver.WorkflowChainRollup, prefix string) []agentresolver.WorkflowChainRollup {
	if len(in) == 0 {
		return nil
	}
	out := make([]agentresolver.WorkflowChainRollup, 0, len(in))
	for _, item := range in {
		copyItem := item
		copyItem.Value = redactValue(prefix, copyItem.Value, 8)
		out = append(out, copyItem)
	}
	return out
}

func sanitizeActionSurfaceRegistryPublic(in []ActionSurfaceRegistryEntry) []ActionSurfaceRegistryEntry {
	if len(in) == 0 {
		return nil
	}
	out := make([]ActionSurfaceRegistryEntry, 0, len(in))
	for _, item := range in {
		copyItem := item
		copyItem.RegistryID = redactValue("surface", copyItem.RegistryID, 8)
		copyItem.Org = redactValue("org", copyItem.Org, 6)
		copyItem.Repo = redactValue("repo", copyItem.Repo, 6)
		copyItem.ToolInstanceID = redactValue("instance", copyItem.ToolInstanceID, 8)
		copyItem.Location = redactValue("loc", copyItem.Location, 8)
		copyItem.Label = redactValue("label", copyItem.Label, 8)
		copyItem.Owner = redactValue("owner", copyItem.Owner, 8)
		copyItem.ConfigSource = redactValue("loc", copyItem.ConfigSource, 8)
		copyItem.PathIDs = redactStringSlice(copyItem.PathIDs, "path")
		copyItem.GraphRefs = AgentActionBOMGraphRefs{
			NodeIDs: redactStringSlice(copyItem.GraphRefs.NodeIDs, "node"),
			EdgeIDs: redactStringSlice(copyItem.GraphRefs.EdgeIDs, "edge"),
		}
		if copyItem.CredentialAuthority != nil {
			copyItem.CredentialAuthority = agginventory.CloneCredentialAuthority(copyItem.CredentialAuthority)
			copyItem.CredentialAuthority.ReasonCodes = redactStringSlice(copyItem.CredentialAuthority.ReasonCodes, "evidence")
		}
		copyItem.Credentials = redactCredentialsPublic(copyItem.Credentials)
		copyItem.MutableEndpointSemantics = sanitizeMutableEndpointSemanticsPublic(copyItem.MutableEndpointSemantics)
		out = append(out, copyItem)
	}
	return out
}

func sanitizeMutableEndpointSemanticsPublic(in []agginventory.MutableEndpointSemantic) []agginventory.MutableEndpointSemantic {
	if len(in) == 0 {
		return nil
	}
	out := agginventory.CloneMutableEndpointSemantics(in)
	for idx := range out {
		out[idx].Operation = redactValue("endpoint", out[idx].Operation, 8)
		out[idx].EvidenceRefs = redactStringSlice(out[idx].EvidenceRefs, "endpoint")
	}
	return out
}

func redactCredentialsPublic(in []*agginventory.CredentialProvenance) []*agginventory.CredentialProvenance {
	out := agginventory.CloneCredentialProvenances(in)
	for idx := range out {
		out[idx].Subject = redactValue("credential", out[idx].Subject, 8)
		out[idx].EvidenceBasis = redactStringSlice(out[idx].EvidenceBasis, "evidence")
		out[idx].EvidenceLocation = redactValue("loc", out[idx].EvidenceLocation, 8)
		out[idx].ClassificationReasons = redactStringSlice(out[idx].ClassificationReasons, "evidence")
	}
	return out
}

func sanitizeExposureGroupsPublic(in []risk.ExposureGroup) []risk.ExposureGroup {
	if len(in) == 0 {
		return nil
	}
	out := make([]risk.ExposureGroup, 0, len(in))
	for _, item := range in {
		copyItem := item
		copyItem.GroupID = redactValue("group", copyItem.GroupID, 8)
		copyItem.Org = redactValue("org", copyItem.Org, 6)
		copyItem.ExecutionIdentity = redactValue("identity", copyItem.ExecutionIdentity, 8)
		copyItem.ExampleRepo = redactValue("repo", copyItem.ExampleRepo, 6)
		copyItem.ExampleLocation = redactValue("loc", copyItem.ExampleLocation, 8)
		copyItem.Repos = redactStringSlice(copyItem.Repos, "repo")
		copyItem.PathIDs = redactStringSlice(copyItem.PathIDs, "path")
		out = append(out, copyItem)
	}
	return out
}

func sanitizeAssessmentSummaryPublic(in *AssessmentSummary) *AssessmentSummary {
	if in == nil {
		return nil
	}
	copySummary := *in
	if in.TopPathToControlFirst != nil {
		paths := sanitizeActionPathsPublic([]risk.ActionPath{*in.TopPathToControlFirst})
		if len(paths) == 1 {
			copySummary.TopPathToControlFirst = &paths[0]
		}
	}
	if in.TopExecutionIdentityBacked != nil {
		paths := sanitizeActionPathsPublic([]risk.ActionPath{*in.TopExecutionIdentityBacked})
		if len(paths) == 1 {
			copySummary.TopExecutionIdentityBacked = &paths[0]
		}
	}
	if in.IdentityToReviewFirst != nil {
		copyTarget := *in.IdentityToReviewFirst
		copyTarget.ExecutionIdentity = redactValue("identity", copyTarget.ExecutionIdentity, 8)
		copySummary.IdentityToReviewFirst = &copyTarget
	}
	if in.IdentityToRevokeFirst != nil {
		copyTarget := *in.IdentityToRevokeFirst
		copyTarget.ExecutionIdentity = redactValue("identity", copyTarget.ExecutionIdentity, 8)
		copySummary.IdentityToRevokeFirst = &copyTarget
	}
	copySummary.ProofChainPath = sanitizeProofReferencePublic(ProofReference{ChainPath: in.ProofChainPath}).ChainPath
	return &copySummary
}

func sanitizeControlPathGraphPublic(in *aggattack.ControlPathGraph) *aggattack.ControlPathGraph {
	if in == nil {
		return nil
	}
	copyGraph := *in
	copyGraph.Nodes = append([]aggattack.ControlPathNode(nil), in.Nodes...)
	copyGraph.Edges = append([]aggattack.ControlPathEdge(nil), in.Edges...)
	for idx := range copyGraph.Nodes {
		copyGraph.Nodes[idx].NodeID = redactValue("node", copyGraph.Nodes[idx].NodeID, 8)
		copyGraph.Nodes[idx].PathID = redactValue("path", copyGraph.Nodes[idx].PathID, 8)
		copyGraph.Nodes[idx].Org = redactValue("org", copyGraph.Nodes[idx].Org, 6)
		copyGraph.Nodes[idx].Repo = redactValue("repo", copyGraph.Nodes[idx].Repo, 6)
		copyGraph.Nodes[idx].Label = redactValue("label", copyGraph.Nodes[idx].Label, 8)
		copyGraph.Nodes[idx].Location = redactValue("loc", copyGraph.Nodes[idx].Location, 8)
		copyGraph.Nodes[idx].AgentID = redactValue("agent", copyGraph.Nodes[idx].AgentID, 8)
		copyGraph.Nodes[idx].ConfigSource = redactValue("loc", copyGraph.Nodes[idx].ConfigSource, 8)
		copyGraph.Nodes[idx].AuthorityBindingRefs = redactStringSlice(copyGraph.Nodes[idx].AuthorityBindingRefs, "binding")
		if copyGraph.Nodes[idx].CredentialAuthority != nil {
			copyGraph.Nodes[idx].CredentialAuthority = agginventory.CloneCredentialAuthority(copyGraph.Nodes[idx].CredentialAuthority)
			copyGraph.Nodes[idx].CredentialAuthority.ReasonCodes = redactStringSlice(copyGraph.Nodes[idx].CredentialAuthority.ReasonCodes, "evidence")
		}
		copyGraph.Nodes[idx].MutableEndpointSemantics = sanitizeMutableEndpointSemanticsPublic(copyGraph.Nodes[idx].MutableEndpointSemantics)
		copyGraph.Nodes[idx].EvidenceRefs = redactStringSlice(copyGraph.Nodes[idx].EvidenceRefs, "evidence")
		copyGraph.Nodes[idx].SourceRefs = redactStringSlice(copyGraph.Nodes[idx].SourceRefs, "source")
		copyGraph.Nodes[idx].AttackPathRefs = redactStringSlice(copyGraph.Nodes[idx].AttackPathRefs, "attack")
		copyGraph.Nodes[idx].SourceFindingKeys = redactStringSlice(copyGraph.Nodes[idx].SourceFindingKeys, "finding")
	}
	for idx := range copyGraph.Edges {
		copyGraph.Edges[idx].EdgeID = redactValue("edge", copyGraph.Edges[idx].EdgeID, 8)
		copyGraph.Edges[idx].PathID = redactValue("path", copyGraph.Edges[idx].PathID, 8)
		copyGraph.Edges[idx].FromNodeID = redactValue("node", copyGraph.Edges[idx].FromNodeID, 8)
		copyGraph.Edges[idx].ToNodeID = redactValue("node", copyGraph.Edges[idx].ToNodeID, 8)
		copyGraph.Edges[idx].EvidenceRefs = redactStringSlice(copyGraph.Edges[idx].EvidenceRefs, "evidence")
		copyGraph.Edges[idx].SourceRefs = redactStringSlice(copyGraph.Edges[idx].SourceRefs, "source")
		copyGraph.Edges[idx].AttackPathRefs = redactStringSlice(copyGraph.Edges[idx].AttackPathRefs, "attack")
		copyGraph.Edges[idx].SourceFindingKeys = redactStringSlice(copyGraph.Edges[idx].SourceFindingKeys, "finding")
	}
	sortControlPathGraphForReport(&copyGraph)
	return &copyGraph
}

func sanitizeControlBacklogPublic(in *controlbacklog.Backlog) *controlbacklog.Backlog {
	if in == nil {
		return nil
	}
	copyBacklog := *in
	copyBacklog.Items = append([]controlbacklog.Item(nil), in.Items...)
	for idx := range copyBacklog.Items {
		copyBacklog.Items[idx].Repo = redactValue("repo", copyBacklog.Items[idx].Repo, 6)
		copyBacklog.Items[idx].Path = redactValue("loc", copyBacklog.Items[idx].Path, 8)
		copyBacklog.Items[idx].Owner = redactValue("owner", copyBacklog.Items[idx].Owner, 8)
		copyBacklog.Items[idx].LinkedFindingIDs = redactStringSlice(copyBacklog.Items[idx].LinkedFindingIDs, "finding")
		copyBacklog.Items[idx].LinkedActionPathID = redactValue("path", copyBacklog.Items[idx].LinkedActionPathID, 8)
		copyBacklog.Items[idx].LinkedControlPathNodeIDs = redactStringSlice(copyBacklog.Items[idx].LinkedControlPathNodeIDs, "node")
		copyBacklog.Items[idx].LinkedControlPathEdgeIDs = redactStringSlice(copyBacklog.Items[idx].LinkedControlPathEdgeIDs, "edge")
		copyBacklog.Items[idx].OwnershipEvidence = redactStringSlice(copyBacklog.Items[idx].OwnershipEvidence, "evidence")
		copyBacklog.Items[idx].OwnershipConflicts = redactStringSlice(copyBacklog.Items[idx].OwnershipConflicts, "owner")
		copyBacklog.Items[idx].ClosureRequirements = sanitizeClosureRequirementsPublic(copyBacklog.Items[idx].ClosureRequirements)
		copyBacklog.Items[idx].EvidenceCompleteness = risk.CloneEvidenceCompleteness(copyBacklog.Items[idx].EvidenceCompleteness)
		copyBacklog.Items[idx].GovernanceDisposition = sanitizeGovernanceDispositionPublic(copyBacklog.Items[idx].GovernanceDisposition)
		copyBacklog.Items[idx].LifecycleQueue = sanitizeLifecycleQueuePublic(copyBacklog.Items[idx].LifecycleQueue)
		if copyBacklog.Items[idx].CredentialProvenance != nil {
			copyBacklog.Items[idx].CredentialProvenance = agginventory.CloneCredentialProvenance(copyBacklog.Items[idx].CredentialProvenance)
			copyBacklog.Items[idx].CredentialProvenance.Subject = redactValue("credential", copyBacklog.Items[idx].CredentialProvenance.Subject, 8)
			copyBacklog.Items[idx].CredentialProvenance.EvidenceBasis = redactStringSlice(copyBacklog.Items[idx].CredentialProvenance.EvidenceBasis, "evidence")
			copyBacklog.Items[idx].CredentialProvenance.EvidenceLocation = redactValue("loc", copyBacklog.Items[idx].CredentialProvenance.EvidenceLocation, 8)
		}
		if copyBacklog.Items[idx].CredentialAuthority != nil {
			copyBacklog.Items[idx].CredentialAuthority = agginventory.CloneCredentialAuthority(copyBacklog.Items[idx].CredentialAuthority)
			copyBacklog.Items[idx].CredentialAuthority.ReasonCodes = redactStringSlice(copyBacklog.Items[idx].CredentialAuthority.ReasonCodes, "evidence")
		}
	}
	return &copyBacklog
}

func sanitizeAgentActionBOM(in *AgentActionBOM, profile ShareProfile) *AgentActionBOM {
	if in == nil {
		return nil
	}
	copyBOM := *in
	copyBOM.ShareProfile = string(profile)
	copyBOM.ShareProfileMetadata = cloneShareProfileMetadata(in.ShareProfileMetadata)
	copyBOM.Summary.EvidenceCompleteness = risk.CloneEvidenceCompletenessSummary(in.Summary.EvidenceCompleteness)
	copyBOM.ScanQuality = sanitizeScanQualityPublic(in.ScanQuality)
	copyBOM.EvidenceRefs = redactStringSlice(in.EvidenceRefs, "evidence")
	copyBOM.ProofRefs = redactStringSlice(in.ProofRefs, "proof")
	copyBOM.GraphRefs = AgentActionBOMGraphRefs{
		NodeIDs: redactStringSlice(in.GraphRefs.NodeIDs, "node"),
		EdgeIDs: redactStringSlice(in.GraphRefs.EdgeIDs, "edge"),
	}
	copyBOM.Summary.PrimaryView = sanitizePrimaryViewPublic(in.Summary.PrimaryView)
	copyBOM.Items = append([]AgentActionBOMItem(nil), in.Items...)
	for idx := range copyBOM.Items {
		copyBOM.Items[idx].PathID = redactValue("path", copyBOM.Items[idx].PathID, 8)
		copyBOM.Items[idx].AgentID = redactValue("agent", copyBOM.Items[idx].AgentID, 8)
		copyBOM.Items[idx].ToolFamilyID = redactValue("family", copyBOM.Items[idx].ToolFamilyID, 8)
		copyBOM.Items[idx].ToolInstanceID = redactValue("instance", copyBOM.Items[idx].ToolInstanceID, 8)
		copyBOM.Items[idx].Org = redactValue("org", copyBOM.Items[idx].Org, 6)
		copyBOM.Items[idx].Repo = redactValue("repo", copyBOM.Items[idx].Repo, 6)
		copyBOM.Items[idx].Location = redactValue("loc", copyBOM.Items[idx].Location, 8)
		copyBOM.Items[idx].Owner = redactValue("owner", copyBOM.Items[idx].Owner, 8)
		copyBOM.Items[idx].ConfigSource = redactValue("loc", copyBOM.Items[idx].ConfigSource, 8)
		copyBOM.Items[idx].OccurrenceRefs = redactStringSlice(copyBOM.Items[idx].OccurrenceRefs, "path")
		copyBOM.Items[idx].ProofRefs = redactStringSlice(copyBOM.Items[idx].ProofRefs, "proof")
		copyBOM.Items[idx].RuntimeSessionRefs = redactStringSlice(copyBOM.Items[idx].RuntimeSessionRefs, "session")
		copyBOM.Items[idx].ObservedChangedFiles = redactStringSlice(copyBOM.Items[idx].ObservedChangedFiles, "file")
		copyBOM.Items[idx].RuntimeEvidenceRefs = redactStringSlice(copyBOM.Items[idx].RuntimeEvidenceRefs, "runtime")
		copyBOM.Items[idx].PolicyRefs = redactStringSlice(copyBOM.Items[idx].PolicyRefs, "policy")
		copyBOM.Items[idx].PolicyEvidenceRefs = redactStringSlice(copyBOM.Items[idx].PolicyEvidenceRefs, "policy")
		copyBOM.Items[idx].ClosureRequirements = sanitizeClosureRequirementsPublic(copyBOM.Items[idx].ClosureRequirements)
		copyBOM.Items[idx].EvidenceCompleteness = risk.CloneEvidenceCompleteness(copyBOM.Items[idx].EvidenceCompleteness)
		copyBOM.Items[idx].GovernanceDisposition = sanitizeGovernanceDispositionPublic(copyBOM.Items[idx].GovernanceDisposition)
		copyBOM.Items[idx].LifecycleQueue = sanitizeLifecycleQueuePublic(copyBOM.Items[idx].LifecycleQueue)
		copyBOM.Items[idx].AttackPathRefs = redactStringSlice(copyBOM.Items[idx].AttackPathRefs, "attack")
		copyBOM.Items[idx].SourceFindingKeys = redactStringSlice(copyBOM.Items[idx].SourceFindingKeys, "finding")
		copyBOM.Items[idx].EvidenceRefs = redactStringSlice(copyBOM.Items[idx].EvidenceRefs, "evidence")
		copyBOM.Items[idx].GraphRefs = AgentActionBOMGraphRefs{
			NodeIDs: redactStringSlice(copyBOM.Items[idx].GraphRefs.NodeIDs, "node"),
			EdgeIDs: redactStringSlice(copyBOM.Items[idx].GraphRefs.EdgeIDs, "edge"),
		}
		copyBOM.Items[idx].Reachability = sanitizeReachabilityPublic(copyBOM.Items[idx].Reachability)
		copyBOM.Items[idx].ReachableServers = sanitizeReachabilityPublic(copyBOM.Items[idx].ReachableServers)
		copyBOM.Items[idx].ReachableTools = sanitizeReachabilityPublic(copyBOM.Items[idx].ReachableTools)
		copyBOM.Items[idx].ReachableEndpoints = sanitizeReachabilityPublic(copyBOM.Items[idx].ReachableEndpoints)
		copyBOM.Items[idx].ReachableTargets = sanitizeReachabilityPublic(copyBOM.Items[idx].ReachableTargets)
		copyBOM.Items[idx].ReachableAPIs = sanitizeReachabilityPublic(copyBOM.Items[idx].ReachableAPIs)
		copyBOM.Items[idx].ReachableAgents = sanitizeReachabilityPublic(copyBOM.Items[idx].ReachableAgents)
		if copyBOM.Items[idx].CredentialProvenance != nil {
			copyBOM.Items[idx].CredentialProvenance = agginventory.CloneCredentialProvenance(copyBOM.Items[idx].CredentialProvenance)
			copyBOM.Items[idx].CredentialProvenance.Subject = redactValue("credential", copyBOM.Items[idx].CredentialProvenance.Subject, 8)
			copyBOM.Items[idx].CredentialProvenance.EvidenceBasis = redactStringSlice(copyBOM.Items[idx].CredentialProvenance.EvidenceBasis, "evidence")
			copyBOM.Items[idx].CredentialProvenance.EvidenceLocation = redactValue("loc", copyBOM.Items[idx].CredentialProvenance.EvidenceLocation, 8)
		}
		if copyBOM.Items[idx].CredentialAuthority != nil {
			copyBOM.Items[idx].CredentialAuthority = agginventory.CloneCredentialAuthority(copyBOM.Items[idx].CredentialAuthority)
			copyBOM.Items[idx].CredentialAuthority.ReasonCodes = redactStringSlice(copyBOM.Items[idx].CredentialAuthority.ReasonCodes, "evidence")
		}
		copyBOM.Items[idx].MutableEndpointSemantics = sanitizeMutableEndpointSemanticsPublic(copyBOM.Items[idx].MutableEndpointSemantics)
		copyBOM.Items[idx].Credentials = redactCredentialsPublic(copyBOM.Items[idx].Credentials)
		copyBOM.Items[idx].ActionLineage = sanitizeActionLineagePublic(copyBOM.Items[idx].ActionLineage)
		copyBOM.Items[idx].IntroducedBy = sanitizeIntroducedByPublic(copyBOM.Items[idx].IntroducedBy)
		copyBOM.Items[idx].AgenticDeliverySystemChange = sanitizeAgenticDeliverySystemChangePublic(copyBOM.Items[idx].AgenticDeliverySystemChange)
		copyBOM.Items[idx].RuntimeProvider = redactValue("provider", copyBOM.Items[idx].RuntimeProvider, 8)
		copyBOM.Items[idx].RuntimeHost = redactValue("provider", copyBOM.Items[idx].RuntimeHost, 8)
		copyBOM.Items[idx].RuntimeKind = redactValue("provider", copyBOM.Items[idx].RuntimeKind, 8)
		copyBOM.Items[idx].ModelProvider = redactValue("provider", copyBOM.Items[idx].ModelProvider, 8)
		copyBOM.Items[idx].ModelVersion = redactValue("provider", copyBOM.Items[idx].ModelVersion, 8)
		copyBOM.Items[idx].ExecutionEnvironment = redactValue("provider", copyBOM.Items[idx].ExecutionEnvironment, 8)
		copyBOM.Items[idx].StateLocationRefs = redactStringSlice(copyBOM.Items[idx].StateLocationRefs, "state")
		copyBOM.Items[idx].StateDigestRefs = redactStringSlice(copyBOM.Items[idx].StateDigestRefs, "digest")
		copyBOM.Items[idx].AgentIdentity = sanitizeAgentIdentityPublic(copyBOM.Items[idx].AgentIdentity)
		copyBOM.Items[idx].DecisionPrecedent = sanitizeDecisionPrecedentPublic(copyBOM.Items[idx].DecisionPrecedent)
		copyBOM.Items[idx].DeliveryControlContext = sanitizeDeliveryControlContextPublic(copyBOM.Items[idx].DeliveryControlContext)
		copyBOM.Items[idx].EvidencePacketRefs = redactStringSlice(copyBOM.Items[idx].EvidencePacketRefs, "packet")
		copyBOM.Items[idx].DecisionTraceRefs = redactStringSlice(copyBOM.Items[idx].DecisionTraceRefs, "proof")
	}
	return &copyBOM
}

func sanitizePrimaryViewPublic(in *AgentActionBOMPrimaryView) *AgentActionBOMPrimaryView {
	if in == nil {
		return nil
	}
	out := *in
	out.PathID = redactValue("path", out.PathID, 8)
	out.PathMap = AgentActionBOMPrimaryPathMap{
		Tool:       redactValue("tool", out.PathMap.Tool, 8),
		RepoPR:     redactValue("label", out.PathMap.RepoPR, 8),
		Workflow:   redactValue("loc", out.PathMap.Workflow, 8),
		Credential: redactValue("credential", out.PathMap.Credential, 8),
		Action:     strings.TrimSpace(out.PathMap.Action),
		Target:     redactValue("target", out.PathMap.Target, 8),
	}
	out.TodayPath = risk.CloneGovernedPathView(in.TodayPath)
	out.RecommendedGovernedPath = risk.CloneGovernedPathView(in.RecommendedGovernedPath)
	out.RecommendedActionContract = risk.CloneRecommendedActionContract(in.RecommendedActionContract)
	out.AgenticDeliverySystemChange = sanitizeAgenticDeliverySystemChangePublic(in.AgenticDeliverySystemChange)
	out.RuntimeProvider = redactValue("provider", in.RuntimeProvider, 8)
	out.RuntimeHost = redactValue("provider", in.RuntimeHost, 8)
	out.RuntimeKind = redactValue("provider", in.RuntimeKind, 8)
	out.ModelProvider = redactValue("provider", in.ModelProvider, 8)
	out.ModelVersion = redactValue("provider", in.ModelVersion, 8)
	out.ExecutionEnvironment = redactValue("provider", in.ExecutionEnvironment, 8)
	out.AgentIdentity = sanitizeAgentIdentityPublic(in.AgentIdentity)
	out.DecisionPrecedent = sanitizeDecisionPrecedentPublic(in.DecisionPrecedent)
	out.DeliveryControlContext = sanitizeDeliveryControlContextPublic(in.DeliveryControlContext)
	out.WorkflowChainRefs = redactStringSlice(in.WorkflowChainRefs, "chain")
	out.GraphRefs = AgentActionBOMGraphRefs{
		NodeIDs: redactStringSlice(in.GraphRefs.NodeIDs, "node"),
		EdgeIDs: redactStringSlice(in.GraphRefs.EdgeIDs, "edge"),
	}
	out.ProofRefs = redactStringSlice(in.ProofRefs, "proof")
	out.EvidencePacketRefs = redactStringSlice(in.EvidencePacketRefs, "packet")
	out.DecisionTraceRefs = redactStringSlice(in.DecisionTraceRefs, "proof")
	out.AppendixRefs = cloneStrings(in.AppendixRefs)
	out.UnresolvedEvidence = cloneStrings(in.UnresolvedEvidence)
	return &out
}

func sanitizeAgenticDeliverySystemChangePublic(in *risk.AgenticDeliverySystemChange) *risk.AgenticDeliverySystemChange {
	if in == nil {
		return nil
	}
	out := risk.CloneAgenticDeliverySystemChange(in)
	out.ChangeID = redactValue("change", out.ChangeID, 8)
	out.ChangedArtifact = redactValue("loc", out.ChangedArtifact, 8)
	out.ReachableTargets = redactStringSlice(out.ReachableTargets, "target")
	out.EvidenceRefs = redactStringSlice(out.EvidenceRefs, "evidence")
	return out
}

func sanitizeAgentIdentityPublic(in *risk.AgentIdentity) *risk.AgentIdentity {
	if in == nil {
		return nil
	}
	out := risk.CloneAgentIdentity(in)
	out.IdentityKey = redactValue("identity", out.IdentityKey, 8)
	out.AgentID = redactValue("identity", out.AgentID, 8)
	out.HumanOwner = redactValue("owner", out.HumanOwner, 8)
	out.DelegatedAuthority = redactValue("authority", out.DelegatedAuthority, 8)
	out.RuntimeProvider = redactValue("provider", out.RuntimeProvider, 8)
	out.RuntimeKind = redactValue("provider", out.RuntimeKind, 8)
	out.ModelProvider = redactValue("provider", out.ModelProvider, 8)
	out.CredentialUsed = redactValue("credential", out.CredentialUsed, 8)
	out.Scope = redactValue("scope", out.Scope, 8)
	return out
}

func sanitizeDecisionPrecedentPublic(in *risk.DecisionPrecedent) *risk.DecisionPrecedent {
	if in == nil {
		return nil
	}
	out := risk.CloneDecisionPrecedent(in)
	out.PrecedentKey = redactValue("precedent", out.PrecedentKey, 8)
	out.DecisionTraceRef = redactValue("proof", out.DecisionTraceRef, 8)
	out.EvidenceRefs = redactStringSlice(out.EvidenceRefs, "evidence")
	return out
}

func sanitizeDeliveryControlContextPublic(in *risk.DeliveryControlContext) *risk.DeliveryControlContext {
	if in == nil {
		return nil
	}
	out := risk.CloneDeliveryControlContext(in)
	out.ResolverRefs = redactStringSlice(out.ResolverRefs, "resolver")
	out.EvalConfigRefs = redactStringSlice(out.EvalConfigRefs, "eval")
	out.SandboxGates = redactStringSlice(out.SandboxGates, "sandbox")
	out.TestGates = redactStringSlice(out.TestGates, "test")
	return out
}

func sanitizeGovernanceDispositionPublic(in *controlbacklog.GovernanceDisposition) *controlbacklog.GovernanceDisposition {
	if in == nil {
		return nil
	}
	copyItem := *in
	copyItem.Issuer = redactValue("owner", copyItem.Issuer, 8)
	copyItem.EvidenceRefs = redactStringSlice(copyItem.EvidenceRefs, "evidence")
	return &copyItem
}

func sanitizeLifecycleQueuePublic(in *governancequeue.Item) *governancequeue.Item {
	if in == nil {
		return nil
	}
	copyItem := *in
	copyItem.AgentID = redactValue("agent", copyItem.AgentID, 8)
	copyItem.Repo = redactValue("repo", copyItem.Repo, 6)
	copyItem.Path = redactValue("loc", copyItem.Path, 8)
	copyItem.Owner = redactValue("owner", copyItem.Owner, 8)
	copyItem.EvidenceRefs = redactStringSlice(copyItem.EvidenceRefs, "evidence")
	copyItem.SourceConflicts = redactStringSlice(copyItem.SourceConflicts, "owner")
	return &copyItem
}

func sanitizeReachabilityPublic(in []AgentActionBOMReachability) []AgentActionBOMReachability {
	if len(in) == 0 {
		return nil
	}
	out := make([]AgentActionBOMReachability, 0, len(in))
	for _, item := range in {
		copyItem := item
		copyItem.EvidenceRefs = redactStringSlice(copyItem.EvidenceRefs, "evidence")
		switch strings.TrimSpace(copyItem.Surface) {
		case "mcp_tool", "a2a_capability":
			// Preserve capability labels because they carry the buyer-facing risk meaning.
		case "mcp_server":
			copyItem.Name = redactValue("server", copyItem.Name, 8)
		case "a2a_agent":
			copyItem.Name = redactValue("agent", copyItem.Name, 8)
		case "reachable_endpoint":
			copyItem.Name = redactValue("endpoint", copyItem.Name, 8)
		case "reachable_target":
			copyItem.Name = redactValue("target", copyItem.Name, 8)
		default:
			copyItem.Name = redactValue("reach", copyItem.Name, 8)
		}
		out = append(out, copyItem)
	}
	return out
}

func sanitizeClosureRequirementsPublic(in []risk.ClosureRequirement) []risk.ClosureRequirement {
	if len(in) == 0 {
		return nil
	}
	out := risk.CloneClosureRequirements(in)
	for idx := range out {
		out[idx].ClosureRefs = redactStringSlice(out[idx].ClosureRefs, "evidence")
	}
	return out
}

func sanitizeActionLineagePublic(in *risk.ActionLineage) *risk.ActionLineage {
	if in == nil {
		return nil
	}
	copyLineage := risk.CloneActionLineage(in)
	for idx := range copyLineage.Segments {
		copyLineage.Segments[idx].SegmentID = redactValue("segment", copyLineage.Segments[idx].SegmentID, 8)
		copyLineage.Segments[idx].Label = redactValue("label", copyLineage.Segments[idx].Label, 8)
		copyLineage.Segments[idx].NodeIDs = redactStringSlice(copyLineage.Segments[idx].NodeIDs, "node")
		copyLineage.Segments[idx].EdgeIDs = redactStringSlice(copyLineage.Segments[idx].EdgeIDs, "edge")
		copyLineage.Segments[idx].EvidenceRefs = redactStringSlice(copyLineage.Segments[idx].EvidenceRefs, "evidence")
	}
	return copyLineage
}

func cloneScanQualityReport(in *scanquality.Report) *scanquality.Report {
	if in == nil {
		return nil
	}
	copyReport := *in
	if in.CompactSummary != nil {
		compact := *in.CompactSummary
		copyReport.CompactSummary = &compact
	}
	copyReport.SuppressedPaths = append([]scanquality.SuppressedPath(nil), in.SuppressedPaths...)
	copyReport.ParseErrors = append([]scanquality.ParseIssue(nil), in.ParseErrors...)
	copyReport.DetectorErrors = append([]detect.DetectorError(nil), in.DetectorErrors...)
	copyReport.Detectors = append([]scanquality.DetectorHealth(nil), in.Detectors...)
	copyReport.AbsenceClaims = append([]scanquality.AbsenceClaim(nil), in.AbsenceClaims...)
	return &copyReport
}

func sanitizeScanQualityPublic(in *scanquality.Report) *scanquality.Report {
	if in == nil {
		return nil
	}
	copyReport := cloneScanQualityReport(in)
	for idx := range copyReport.SuppressedPaths {
		copyReport.SuppressedPaths[idx].Org = redactValue("org", copyReport.SuppressedPaths[idx].Org, 6)
		copyReport.SuppressedPaths[idx].Repo = redactValue("repo", copyReport.SuppressedPaths[idx].Repo, 6)
		copyReport.SuppressedPaths[idx].Path = redactValue("loc", copyReport.SuppressedPaths[idx].Path, 8)
	}
	for idx := range copyReport.ParseErrors {
		copyReport.ParseErrors[idx].Org = redactValue("org", copyReport.ParseErrors[idx].Org, 6)
		copyReport.ParseErrors[idx].Repo = redactValue("repo", copyReport.ParseErrors[idx].Repo, 6)
		copyReport.ParseErrors[idx].Path = redactValue("loc", copyReport.ParseErrors[idx].Path, 8)
	}
	for idx := range copyReport.Detectors {
		copyReport.Detectors[idx].Org = redactValue("org", copyReport.Detectors[idx].Org, 6)
		copyReport.Detectors[idx].Repo = redactValue("repo", copyReport.Detectors[idx].Repo, 6)
	}
	for idx := range copyReport.DetectorErrors {
		copyReport.DetectorErrors[idx].Org = redactValue("org", copyReport.DetectorErrors[idx].Org, 6)
		copyReport.DetectorErrors[idx].Repo = redactValue("repo", copyReport.DetectorErrors[idx].Repo, 6)
	}
	for idx := range copyReport.AbsenceClaims {
		copyReport.AbsenceClaims[idx].Org = redactValue("org", copyReport.AbsenceClaims[idx].Org, 6)
		copyReport.AbsenceClaims[idx].Repo = redactValue("repo", copyReport.AbsenceClaims[idx].Repo, 6)
	}
	return copyReport
}

func redactStringSlice(values []string, prefix string) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		redacted := redactValue(prefix, value, 8)
		if redacted == "" {
			continue
		}
		out = append(out, redacted)
	}
	return out
}

func sortControlPathGraphForReport(graph *aggattack.ControlPathGraph) {
	if graph == nil {
		return
	}
	sort.Slice(graph.Nodes, func(i, j int) bool {
		if graph.Nodes[i].PathID != graph.Nodes[j].PathID {
			return graph.Nodes[i].PathID < graph.Nodes[j].PathID
		}
		if graph.Nodes[i].Kind != graph.Nodes[j].Kind {
			return graph.Nodes[i].Kind < graph.Nodes[j].Kind
		}
		if graph.Nodes[i].NodeID != graph.Nodes[j].NodeID {
			return graph.Nodes[i].NodeID < graph.Nodes[j].NodeID
		}
		return graph.Nodes[i].Label < graph.Nodes[j].Label
	})
	sort.Slice(graph.Edges, func(i, j int) bool {
		if graph.Edges[i].PathID != graph.Edges[j].PathID {
			return graph.Edges[i].PathID < graph.Edges[j].PathID
		}
		if graph.Edges[i].Kind != graph.Edges[j].Kind {
			return graph.Edges[i].Kind < graph.Edges[j].Kind
		}
		if graph.Edges[i].FromNodeID != graph.Edges[j].FromNodeID {
			return graph.Edges[i].FromNodeID < graph.Edges[j].FromNodeID
		}
		if graph.Edges[i].ToNodeID != graph.Edges[j].ToNodeID {
			return graph.Edges[i].ToNodeID < graph.Edges[j].ToNodeID
		}
		return graph.Edges[i].EdgeID < graph.Edges[j].EdgeID
	})
}

func redactValue(prefix, value string, width int) string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return ""
	}
	sum := sha256.Sum256([]byte(trimmed))
	hex := fmt.Sprintf("%x", sum)
	if width <= 0 || width > len(hex) {
		width = len(hex)
	}
	return fmt.Sprintf("%s-%s", prefix, hex[:width])
}
