diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 15698b54c7..82eb9510a2 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -156,6 +156,9 @@ at each resolved mention site rather than going through an AI-local wrapper. Chat knowledge extraction and resource-context rendering now also consume the shared unified-resource label helpers directly, so governed labels and redacted values stay consistent without AI-local presentation shims. +Those same paths also use the shared resource display-name helper, so the +name-or-ID fallback stays aligned across chat extraction, resource context, +and unified adapter presentation. That shared presentation layer also owns the elapsed-time and "ago" wording utilities, so the same "time ago" phrasing stays consistent across resource, incident, and fallback memory summaries instead of being reformatted diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index ed95ad9f0c..cb88242e7f 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -192,6 +192,9 @@ shared unified-resource contract, and their governed labels now flow through shared `ResourcePolicyLabel(...)` and `ResourcePolicyRedactedValue(...)` helpers instead of through AI-local presentation shims, so policy copying and policy-bound labels both stay centralized in the resource model. +That same contract also owns the canonical resource display-name fallback, so +name-or-ID presentation stays consistent between the unified AI adapter and +the AI resource context instead of being recomputed locally. That same shared store now also persists append-only action lifecycle, action audit, and export audit records, giving the control-plane verbs a durable home diff --git a/internal/ai/resource_context.go b/internal/ai/resource_context.go index baa60e6405..afea102a57 100644 --- a/internal/ai/resource_context.go +++ b/internal/ai/resource_context.go @@ -167,7 +167,7 @@ func (s *Service) buildUnifiedResourceContextForModel(destinationModel string) s sortResources := func(resources []unifiedresources.Resource) { sort.Slice(resources, func(i, j int) bool { - return strings.ToLower(unifiedResourceDisplayName(resources[i])) < strings.ToLower(unifiedResourceDisplayName(resources[j])) + return strings.ToLower(unifiedresources.ResourceDisplayName(resources[i])) < strings.ToLower(unifiedresources.ResourceDisplayName(resources[j])) }) } @@ -347,7 +347,7 @@ func (s *Service) buildUnifiedResourceContextForModel(destinationModel string) s sections = append(sections, fmt.Sprintf("\n**On %s:**", parentName)) children := byParent[parentID] sort.Slice(children, func(i, j int) bool { - return strings.ToLower(unifiedResourceDisplayName(children[i])) < strings.ToLower(unifiedResourceDisplayName(children[j])) + return strings.ToLower(unifiedresources.ResourceDisplayName(children[i])) < strings.ToLower(unifiedresources.ResourceDisplayName(children[j])) }) for _, workload := range children { @@ -375,7 +375,7 @@ func (s *Service) buildUnifiedResourceContextForModel(destinationModel string) s if len(noParent) > 0 { sort.Slice(noParent, func(i, j int) bool { - return strings.ToLower(unifiedResourceDisplayName(noParent[i])) < strings.ToLower(unifiedResourceDisplayName(noParent[j])) + return strings.ToLower(unifiedresources.ResourceDisplayName(noParent[i])) < strings.ToLower(unifiedresources.ResourceDisplayName(noParent[j])) }) sections = append(sections, "\n**Other workloads:**") for _, workload := range noParent { @@ -546,13 +546,6 @@ func (s *Service) buildUnifiedResourceContextForModel(destinationModel string) s return "" } -func unifiedResourceDisplayName(r unifiedresources.Resource) string { - if strings.TrimSpace(r.Name) != "" { - return strings.TrimSpace(r.Name) - } - return strings.TrimSpace(r.ID) -} - func unifiedResourceContextClusterName(r unifiedresources.Resource) string { name := "" switch { diff --git a/internal/ai/resource_context_test.go b/internal/ai/resource_context_test.go index 6bc3b4151c..504811ea58 100644 --- a/internal/ai/resource_context_test.go +++ b/internal/ai/resource_context_test.go @@ -400,6 +400,15 @@ func TestResourcePolicyHelpersShareAISafeSummaryDecision(t *testing.T) { } } +func TestUnifiedResourceDisplayNameUsesSharedHelper(t *testing.T) { + if got := unifiedresources.ResourceDisplayName(unifiedresources.Resource{Name: " node-a ", ID: "id-a"}); got != "node-a" { + t.Fatalf("ResourceDisplayName() with name = %q, want node-a", got) + } + if got := unifiedresources.ResourceDisplayName(unifiedresources.Resource{Name: " ", ID: " id-b "}); got != "id-b" { + t.Fatalf("ResourceDisplayName() with fallback ID = %q, want id-b", got) + } +} + func TestBuildUnifiedResourceContext_UnifiedPath(t *testing.T) { clusterID := "k8s-cluster-1" k8sCluster := unifiedresources.Resource{ diff --git a/internal/unifiedresources/code_standards_test.go b/internal/unifiedresources/code_standards_test.go index 740891a538..2a47eeeb7b 100644 --- a/internal/unifiedresources/code_standards_test.go +++ b/internal/unifiedresources/code_standards_test.go @@ -325,11 +325,17 @@ func TestResourcePolicyLabelHelpersUsedByAIConsumers(t *testing.T) { filepath.Join("..", "ai", "resource_context.go"): { "unifiedresources.ResourcePolicyLabel(", "unifiedresources.ResourcePolicyRedactedValue(", + "unifiedresources.ResourceDisplayName(", + }, + filepath.Join("..", "unifiedresources", "unified_ai_adapter.go"): { + "ResourceDisplayName(results[i])", + "ResourceDisplayName(results[j])", }, filepath.Join(".", "policy_presentation.go"): { "func ResourcePolicyLabel(name, aiSafeSummary string, policy *ResourcePolicy) string", "func ResourcePolicyRedactedValue(value string, policy *ResourcePolicy, hints ...ResourceRedactionHint) string", "const ResourcePolicyRedactedLabel = \"redacted by policy\"", + "func ResourceDisplayName(resource Resource) string", }, } for name, snippets := range requiredSnippets { diff --git a/internal/unifiedresources/policy_metadata_test.go b/internal/unifiedresources/policy_metadata_test.go index 915bc7838c..9b3162cd2b 100644 --- a/internal/unifiedresources/policy_metadata_test.go +++ b/internal/unifiedresources/policy_metadata_test.go @@ -236,6 +236,18 @@ func TestResourcePolicyLabelHelpers(t *testing.T) { } } +func TestResourceDisplayName(t *testing.T) { + if got := ResourceDisplayName(Resource{Name: " node-a ", ID: "id-a"}); got != "node-a" { + t.Fatalf("ResourceDisplayName() with name = %q, want node-a", got) + } + if got := ResourceDisplayName(Resource{Name: " ", ID: " id-b "}); got != "id-b" { + t.Fatalf("ResourceDisplayName() with fallback ID = %q, want id-b", got) + } + if got := ResourceDisplayName(Resource{}); got != "" { + t.Fatalf("ResourceDisplayName() empty resource = %q, want empty string", got) + } +} + func TestCloneResourcePolicy(t *testing.T) { t.Parallel() diff --git a/internal/unifiedresources/policy_presentation.go b/internal/unifiedresources/policy_presentation.go index d00d038737..28de6a4014 100644 --- a/internal/unifiedresources/policy_presentation.go +++ b/internal/unifiedresources/policy_presentation.go @@ -226,6 +226,14 @@ func ResourcePolicyRedactedValue(value string, policy *ResourcePolicy, hints ... return value } +// ResourceDisplayName returns the canonical resource display name fallback. +func ResourceDisplayName(resource Resource) string { + if name := strings.TrimSpace(resource.Name); name != "" { + return name + } + return strings.TrimSpace(resource.ID) +} + // ResourcePolicyUsesAISafeSummary reports whether the canonical aiSafeSummary // should be used instead of raw resource labels for governed output. func ResourcePolicyUsesAISafeSummary(summary string, policy *ResourcePolicy) bool { diff --git a/internal/unifiedresources/unified_ai_adapter.go b/internal/unifiedresources/unified_ai_adapter.go index adfc228399..bc56c6ab2c 100644 --- a/internal/unifiedresources/unified_ai_adapter.go +++ b/internal/unifiedresources/unified_ai_adapter.go @@ -250,7 +250,7 @@ func (a *UnifiedAIAdapter) getTopByMetric(limit int, types []ResourceType, metri left := metric(results[i]) right := metric(results[j]) if left == right { - return strings.ToLower(unifiedDisplayName(results[i])) < strings.ToLower(unifiedDisplayName(results[j])) + return strings.ToLower(ResourceDisplayName(results[i])) < strings.ToLower(ResourceDisplayName(results[j])) } return left > right }) @@ -277,13 +277,6 @@ func metricPercent(metric *MetricValue) float64 { return 0 } -func unifiedDisplayName(resource Resource) string { - if strings.TrimSpace(resource.Name) != "" { - return resource.Name - } - return resource.ID -} - func isUnifiedInfrastructure(t ResourceType) bool { switch t { case ResourceTypeAgent, ResourceTypeK8sNode, ResourceTypeK8sCluster: