Share recent change section formatting

This commit is contained in:
rcourtman
2026-03-19 02:53:50 +00:00
parent 85914c21b9
commit 399f5d64dc
9 changed files with 96 additions and 35 deletions
@@ -172,6 +172,10 @@ The canonical recent-change sentence formatting also lives in
`internal/unifiedresources.FormatResourceChangeSummary`, so AI runtime prompt
sections and Patrol seed context reuse the same change wording instead of
keeping another lane-local formatter.
The canonical recent-change section wrapper also lives in
`internal/unifiedresources.FormatResourceRecentChangesContext`, so the AI
summary and resource-specific context share the same heading and prefix rules
instead of rebuilding that section layout locally.
The related-resource correlation section now also comes from the shared
correlation formatter in `internal/ai/correlation`, so resource chat and
incident prompts reuse the same learned-edge wording instead of rebuilding a
@@ -152,6 +152,9 @@ That same helper now also owns the one-line recent-change summary text used
by the AI runtime prompt sections and Patrol seed context, so the change
wording itself stays canonical before the surrounding section headers are
applied.
The same helper now also owns the canonical recent-change section wrapper,
so the Patrol page and AI runtime can share the same heading and resource
prefix rules instead of rebuilding that section locally.
The canonical shared AI resource context now also surfaces policy routing and
redaction hints from unified resources, so the Patrol page and resource drawer
see the same governance posture that the runtime uses for export boundaries.
@@ -101,6 +101,9 @@ That same change-presentation helper now also owns the one-line
`FormatResourceChangeSummary` used by AI runtime recent-change sections and
Patrol seed context, so the change wording itself stays canonical before any
section-specific headings are applied.
The same helper also owns `FormatResourceRecentChangesContext`, so AI runtime
callers share the canonical recent-change section heading and resource
prefixing instead of rebuilding that wrapper locally.
The backend AI and Patrol graph context renderers now derive their canonical
relationship labels, direction, provenance, freshness, and metadata flags
from `internal/unifiedresources/relationship_presentation.go`, so the graph
+1 -24
View File
@@ -518,7 +518,7 @@ func (i *Intelligence) buildRecentChangesContext(resourceID string, resourceTime
if resourceTimelineStore != nil {
if recent, err := resourceTimelineStore.GetRecentChanges(resourceID, since, limit); err == nil && len(recent) > 0 {
return formatCanonicalRecentChangesContext(recent, includeResourcePrefix)
return unifiedresources.FormatResourceRecentChangesContext(recent, includeResourcePrefix, "##")
}
}
@@ -538,29 +538,6 @@ func (i *Intelligence) buildRecentChangesContext(resourceID string, resourceTime
return formatMemoryRecentChangesContext(recent, includeResourcePrefix)
}
func formatCanonicalRecentChangesContext(changes []unifiedresources.ResourceChange, includeResourcePrefix bool) string {
if len(changes) == 0 {
return ""
}
heading := "\n## Recent Changes"
if includeResourcePrefix {
heading = "\n## Recent Changes Across Infrastructure"
}
lines := []string{heading, "What changed recently:"}
for _, change := range changes {
entry := unifiedresources.FormatResourceChangeSummary(change)
if includeResourcePrefix {
if resourceID := strings.TrimSpace(change.ResourceID); resourceID != "" {
entry = fmt.Sprintf("%s: %s", resourceID, entry)
}
}
lines = append(lines, "- "+entry)
}
return strings.Join(lines, "\n")
}
func formatMemoryRecentChangesContext(changes []memory.Change, includeResourcePrefix bool) string {
if len(changes) == 0 {
return ""
+27
View File
@@ -458,6 +458,33 @@ func TestIntelligence_FormatGlobalContext_FallsBackToChangeDetector(t *testing.T
}
}
func TestIntelligence_BuildRecentChangesContext_UsesCanonicalSectionFormatter(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
store := ur.NewMemoryStore()
if err := store.RecordChange(ur.ResourceChange{
ID: "change-1",
ObservedAt: time.Now().Add(-time.Hour),
ResourceID: "node-1",
Kind: ur.ChangeRestart,
SourceType: ur.SourcePlatformEvent,
SourceAdapter: ur.AdapterProxmox,
Reason: "node restarted after maintenance",
}); err != nil {
t.Fatalf("record canonical change: %v", err)
}
ctx := intel.buildRecentChangesContext("node-1", store, nil, true, 5)
for _, want := range []string{
"## Recent Changes Across Infrastructure",
"node-1: **Restart**",
"platform_event/proxmox_adapter",
} {
if !strings.Contains(ctx, want) {
t.Fatalf("expected canonical recent-changes context %q to contain %q", ctx, want)
}
}
}
func TestIntelligence_CreatePredictionFinding_LowSeverity(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
+4 -8
View File
@@ -4582,15 +4582,11 @@ func (s *Service) buildRecentResourceChangesContext(resourceID string) string {
Str("resource_id", resourceID).
Msg("failed to load canonical resource timeline context")
} else if len(changes) > 0 {
var changeInfo []string
for _, change := range changes {
if len(changeInfo) >= 3 {
break
}
changeInfo = append(changeInfo, unifiedresources.FormatResourceChangeSummary(change))
if len(changes) > 3 {
changes = changes[:3]
}
if len(changeInfo) > 0 {
return "\n\n### Recent Changes\n" + strings.Join(changeInfo, "\n")
if section := unifiedresources.FormatResourceRecentChangesContext(changes, false, "###"); section != "" {
return section
}
}
}
@@ -114,3 +114,31 @@ func FormatResourceChangeSummary(change ResourceChange) string {
}
return summary + fmt.Sprintf(" (%s ago)", ago)
}
// FormatResourceRecentChangesContext returns the canonical markdown section
// used by AI prompt surfaces for recent unified-resource changes.
func FormatResourceRecentChangesContext(changes []ResourceChange, includeResourcePrefix bool, headingLevel string) string {
if len(changes) == 0 {
return ""
}
if strings.TrimSpace(headingLevel) == "" {
headingLevel = "##"
}
heading := fmt.Sprintf("\n%s Recent Changes", headingLevel)
if includeResourcePrefix {
heading = fmt.Sprintf("\n%s Recent Changes Across Infrastructure", headingLevel)
}
lines := []string{heading, "What changed recently:"}
for _, change := range changes {
entry := FormatResourceChangeSummary(change)
if includeResourcePrefix {
if resourceID := strings.TrimSpace(change.ResourceID); resourceID != "" {
entry = fmt.Sprintf("%s: %s", resourceID, entry)
}
}
lines = append(lines, "- "+entry)
}
return strings.Join(lines, "\n")
}
@@ -92,3 +92,27 @@ func TestFormatResourceChangeSummary(t *testing.T) {
}
}
}
func TestFormatResourceRecentChangesContext(t *testing.T) {
changes := []ResourceChange{
{
Kind: ChangeRestart,
ResourceID: "vm-1",
SourceType: SourcePlatformEvent,
SourceAdapter: AdapterProxmox,
Reason: "maintenance",
ObservedAt: time.Now().Add(-time.Hour),
},
}
ctx := FormatResourceRecentChangesContext(changes, true, "###")
for _, want := range []string{
"### Recent Changes Across Infrastructure",
"vm-1: **Restart**",
"platform_event/proxmox_adapter",
} {
if !strings.Contains(ctx, want) {
t.Fatalf("expected recent changes context %q to contain %q", ctx, want)
}
}
}
@@ -374,7 +374,7 @@ func TestResourceGraphContextUsesCanonicalRelationshipPresentation(t *testing.T)
"func (s *Service) buildResourceGraphContext(resourceID string) string",
"if graphContext := s.buildResourceGraphContext(resourceID); graphContext != \"\" {",
"unifiedresources.FormatResourceGraphContext(resource, 3)",
"unifiedresources.FormatResourceChangeSummary(change)",
"unifiedresources.FormatResourceRecentChangesContext(changes, false, \"###\")",
"type canonicalResourceGetter interface {",
"correlationDetector.FormatForContext(resourceID)",
}
@@ -410,8 +410,7 @@ func TestIntelligenceRecentChangesUseCanonicalSummaryFormatter(t *testing.T) {
}
source := string(data)
requiredSnippets := []string{
"unifiedresources.FormatResourceChangeSummary(change)",
"formatCanonicalRecentChangesContext(recent, includeResourcePrefix)",
"unifiedresources.FormatResourceRecentChangesContext(recent, includeResourcePrefix, \"##\")",
}
for _, snippet := range requiredSnippets {
if !strings.Contains(source, snippet) {