From cc806171dc8f4b19ef1069f0db0c93cee1dfd69f Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 19 Mar 2026 14:26:30 +0000 Subject: [PATCH] Trim dead resource graph surface --- .../v6/internal/subsystems/agent-lifecycle.md | 4 +- .../v6/internal/subsystems/ai-runtime.md | 14 ++--- .../v6/internal/subsystems/api-contracts.md | 7 ++- .../subsystems/patrol-intelligence.md | 8 +-- .../internal/subsystems/storage-recovery.md | 4 +- .../internal/subsystems/unified-resources.md | 53 +++++++--------- .../__tests__/useUnifiedResources.test.ts | 6 ++ frontend-modern/src/types/resource.ts | 39 ------------ .../frontendResourceTypeBoundaries.test.ts | 4 -- .../resourceRelationshipPresentation.test.ts | 40 ------------ .../utils/resourceRelationshipPresentation.ts | 43 ------------- internal/ai/chat/service.go | 9 --- internal/ai/chat/service_additional_test.go | 1 - internal/ai/tools/executor.go | 10 +-- internal/ai/tools/tools_knowledge.go | 60 +----------------- internal/api/ai_handler.go | 1 - internal/api/ai_handler_test.go | 1 - internal/api/contract_test.go | 63 +------------------ internal/api/resources.go | 23 ++----- internal/api/resources_test.go | 32 +++------- internal/api/router.go | 1 - internal/unifiedresources/clone.go | 2 - .../unifiedresources/code_standards_test.go | 33 ++++++++++ internal/unifiedresources/types.go | 2 - 24 files changed, 100 insertions(+), 360 deletions(-) delete mode 100644 frontend-modern/src/utils/__tests__/resourceRelationshipPresentation.test.ts delete mode 100644 frontend-modern/src/utils/resourceRelationshipPresentation.ts diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 93f820152..99ecca04e 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -151,8 +151,8 @@ surfaces can keep row summaries aligned without re-inferring totals from consumer-local slices. That same shared facet bundle now also carries grouped `recentChangeKinds` counts by canonical change kind, so the lifecycle-adjacent detail surfaces can -report restart, anomaly, relationship, and capability distribution without -rebuilding timeline math in the browser. +report restart, anomaly, and other timeline distribution without rebuilding +timeline math in the browser. That same shared facet bundle now also carries grouped `recentChangeSourceTypes` counts by canonical source type, so the lifecycle-adjacent detail surfaces can distinguish platform events, pulse diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index dfd32b458..7614aea5e 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -265,13 +265,13 @@ 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 confidence percentage wording used by the drawer's relationship and -change timeline rows also flows through a shared frontend formatter, so the -same `50%`-style labels stay consistent across resource-graph surfaces -instead of being re-derived in the component. -The remaining fallback token humanization used by those same resource-graph -surfaces also flows through one shared frontend helper, so the title-casing -and underscore cleanup used for relationship, change, and drawer labels stay +The confidence percentage wording used by the drawer's change timeline rows +also flows through a shared frontend formatter, so the same `50%`-style +labels stay consistent across timeline surfaces instead of being re-derived +in the component. +The remaining fallback token humanization used by those same timeline and +drawer surfaces also flows through one shared frontend helper, so the +title-casing and underscore cleanup used for change and drawer labels stay centralized instead of being reimplemented locally. The canonical recent-change section wrapper also lives in `internal/unifiedresources.FormatResourceRecentChangesContext`, so the AI diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 6f70cc3da..8039496eb 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -75,7 +75,7 @@ Own canonical runtime payload shapes between backend and frontend. 3. Add dedicated contract tests for new stable payloads 4. Route unified resource sensitivity, routing, and `aiSafeSummary` payload changes through `internal/api/resources.go`, `internal/api/contract_test.go`, and the canonical frontend resource consumer proofs together; resource governance metadata must not ship as an API-only or frontend-only heuristic 5. Route unified-resource action, lifecycle, and export audit reads through `internal/api/activity_audit_handlers.go`, `internal/api/router_routes_licensing.go`, and `internal/api/contract_test.go` together so the control-plane execution trail stays on a governed API contract instead of a store-only shape -6. Route dedicated unified-resource capability, relationship, timeline, and facet-bundle reads through `frontend-modern/src/api/resources.ts`, `internal/api/resources.go`, and `internal/api/contract_test.go` together so the backend facet contract and the frontend client stay aligned on one governed surface, including the backend-provided facet counts needed to distinguish loaded slices from total history +6. Route dedicated unified-resource timeline and facet-bundle reads through `frontend-modern/src/api/resources.ts`, `internal/api/resources.go`, and `internal/api/contract_test.go` together so the backend facet contract and the frontend client stay aligned on one timeline-first surface, while capability and relationship detail stays backend-owned for AI correlation and change detection 7. Route canonical AI intelligence summary and resource-intelligence reads through `frontend-modern/src/api/ai.ts`, `frontend-modern/src/stores/aiIntelligence.ts`, `frontend-modern/src/pages/AIIntelligence.tsx`, `internal/api/ai_handlers.go`, and `internal/api/contract_test.go` together so the summary card, store state, and backend payload stay aligned on one governed surface, including the canonical recent-changes slice and the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` card, so canonical recent-change timelines stay rendered through one governed frontend card instead of separate page-local list loops and the shared `frontend-modern/src/utils/resourceChangePresentation.ts` formatter used by the summary page and resource drawer, so canonical change wording does not drift across surfaces @@ -132,8 +132,9 @@ The unified resource API payload now carries the richer domain facets directly through the owned backend response: resource objects can expose canonical `capabilities`, `relationships`, `recentChanges`, and derived `facetCounts` in addition to policy and identity metadata, so the backend payload contract -stays aligned with the -timeline and control-plane model instead of flattening those fields away. +stays aligned with the timeline and control-plane model instead of flattening +those fields away. The frontend consumer, however, only preserves the +timeline-first `recentChanges` slice and its counts on the bundle contract. The same resource contract now also exposes a dedicated `/api/resources/{id}/timeline` history endpoint and bundled facet reads under `/api/resources/{id}/facets`, so operators can inspect change history without diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 338bc53f1..4e00c8112 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -159,10 +159,10 @@ The Patrol page and resource drawer now also share the canonical `frontend-modern/src/utils/resourceChangePresentation.ts` formatter so recent-change kind and headline wording stays aligned wherever the canonical timeline is surfaced. -The Patrol page and resource drawer now also share a frontend relationship -presentation helper for graph labels and provenance wording, so the same -canonical relationship semantics render consistently across the resource and -intelligence surfaces. +The Patrol page and resource drawer now keep canonical relationship semantics +in the backend correlation path, while the visible frontend stays +timeline-first and does not surface a separate relationship presentation +helper. The backend Patrol and AI runtime summaries now also share `internal/unifiedresources/change_presentation.go` for the canonical change-kind and provenance mapping, so the same resource-model semantics diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 8a4a16b2e..91f8fdf43 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -167,8 +167,8 @@ pages do not hit a missing-provider 500 before the monitor is fully wired. The shared unified-resource consumer hook now also preserves `recentChanges`, `facetCounts`, `policy`, and `aiSafeSummary` fields when storage and recovery surfaces read unified resources, so those pages see the same control-plane -timeline facets as the dedicated resource drawer instead of flattening them -away locally. +timeline facets and recent-change totals as the dedicated resource drawer +instead of flattening them away locally. The same storage-facing runtime paths now also normalize org scope through `frontend-modern/src/utils/orgScope.ts` before building cache keys or multi-tenant fetch state, so Dashboard, StorageSummary, and other storage diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index ebbb79424..f89075e60 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -169,13 +169,12 @@ The canonical unified-resource change and relationship presenters now also share the same elapsed-time and "ago" wording utilities, so `observed`, `last seen`, and `ago` fragments stay consistent without each formatter maintaining its own "time ago" implementation. -The drawer's relationship and change timeline confidence labels now also use -a shared frontend formatter, so the same percentage wording is emitted across -resource-graph surfaces instead of each consumer rounding confidence values on -its own. -Those same resource-graph surfaces now also share a token-humanization helper -for fallback labels, so underscore cleanup and title-casing for relationship, -change, and drawer labels stay aligned without local copies. +The drawer's change timeline confidence labels now also use a shared frontend +formatter, so the same percentage wording is emitted across timeline +surfaces instead of each consumer rounding confidence values on its own. +Those same timeline surfaces now also share a token-humanization helper for +fallback labels, so underscore cleanup and title-casing for change and drawer +labels stay aligned without local copies. The same resource-change contract now also owns the canonical filter parser used by `/api/resources/{id}/timeline`, so `kind`, `sourceType`, and `sourceAdapter` validation stays with the change model instead of being @@ -213,15 +212,12 @@ drawer, which keeps the presentation surface aligned with the governed API contract instead of rebuilding the graph and timeline inline. The shared `ResourceFacetSummary` consumer now omits capability and relationship badges from the default table/detail surface entirely, while the -backend contract still preserves those facet fields for governed consumers. +backend contract keeps capability and relationship data on the owned resource +model for governed AI and correlation consumers. That keeps the proven monitoring UX centered on factual timeline investigation while the richer facet payloads remain available as backend and AI-facing foundations instead of being presented as first-class product facts before they are fully populated. -That drawer now also uses a shared frontend relationship-presentation helper -for graph labels and provenance wording, so the UI stays aligned with the -canonical relationship semantics instead of keeping drawer-local token -humanization. The same facet bundle now also returns grouped recent-change counts by canonical change kind, so the detail drawer can surface the distribution of state transitions, restarts, config updates, and anomalies without @@ -355,10 +351,10 @@ canonical `facetCounts` on the resource object when available, so the backend list/read shapes remain the source of truth instead of forcing the frontend to infer totals only from loaded slices. The drawer now fetches those facets through one backend bundle endpoint, and that shared facet bundle preserves -backend counts for the hidden capability and relationship model alongside the -timeline slice so the overview card and history summary can report the total -facet history instead of collapsing to the currently loaded page when the -timeline endpoint is paginated. Timeline references in that drawer now route +the timeline slice plus recent-change counts so the overview card and history +summary can report the loaded history instead of collapsing to the currently +loaded page when the timeline endpoint is paginated. Timeline references in +that drawer now route through the canonical infrastructure resource filter, so the resource history remains navigable from the history surface instead of being purely descriptive text. @@ -366,10 +362,9 @@ descriptive text. `frontend-modern/src/utils/resourceChangePresentation.ts` label helper for canonical change kinds, source types, and adapter provenance, so the chip wording stays aligned across table, drawer, and intelligence surfaces. -Relationship cards in that drawer also surface `lastSeenAt` freshness and -optional metadata blocks, and timeline cards surface change metadata when it -is present, so the graph history view preserves the richer provenance already -carried by the unified-resource model instead of flattening those fields away. +Timeline cards in that drawer surface change metadata when it is present, so +the history view preserves the richer provenance already carried by the +unified-resource model instead of flattening those fields away. The same Infrastructure resource-only links now also default through the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` and `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` @@ -539,15 +534,15 @@ They also own the canonical sensitivity and routing order used to format policy-posture count summaries with human-readable labels, so the AI summary and frontend policy card both read the same presentation sequence from the shared resource model. -Canonical resources now carry first-class graph-expansion fields: `Capabilities` -(bounded action definitions with approval levels), `Relationships` (typed -inter-resource links with direction and confidence), and `RecentChanges` (typed -change timeline entries with source, confidence, and related-resource -references). These fields are defined in `capabilities.go`, `relationships.go`, -`changes.go`, `privacy.go`, and `actions.go`. The frontend capability drawer -now formats the shared approval-level vocabulary through a canonical -presentation helper instead of a local switch, so the resource model and the -rendered labels stay aligned. The store now also owns a +Canonical resources now carry first-class graph-expansion fields: +`Capabilities` (bounded action definitions with approval levels), +`Relationships` (typed inter-resource links with direction and confidence), +and `RecentChanges` (typed change timeline entries with source, confidence, +and related-resource references). These fields are defined in +`capabilities.go`, `relationships.go`, `changes.go`, `privacy.go`, and +`actions.go`. The backend keeps those graph fields for AI and correlation use, +while the frontend consumer stays timeline-first and only preserves the +recent-change slice plus facet counts it actually renders. The store now also owns a `resource_changes` persistence table with `RecordChange` and `GetRecentChanges` methods so change history is queryable by canonical ID and time window. The shared change presentation helper also owns the canonical kind, source diff --git a/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts b/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts index 652d51c8e..0f56c5d66 100644 --- a/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts +++ b/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts @@ -16,6 +16,9 @@ const v2Resource = { status: 'online', lastSeen: '2026-02-06T12:00:00Z', sources: ['agent'], + facetCounts: { + recentChanges: 1, + }, metrics: { cpu: { percent: 15 }, memory: { used: 4 * 1024 * 1024, total: 8 * 1024 * 1024, percent: 50 }, @@ -193,6 +196,9 @@ describe('useUnifiedResources', () => { resourceId: 'host-1', hostname: 'pve1', }); + expect(result!.resources()[0].facetCounts).toEqual({ + recentChanges: 1, + }); dispose(); }); diff --git a/frontend-modern/src/types/resource.ts b/frontend-modern/src/types/resource.ts index 5e6926645..4cd832403 100644 --- a/frontend-modern/src/types/resource.ts +++ b/frontend-modern/src/types/resource.ts @@ -138,14 +138,7 @@ export interface ResourcePolicy { routing: ResourceRoutingPolicy; } -export type ResourceCapabilityType = 'common' | 'native'; export type ResourceApprovalLevel = 'none' | 'dry_run_only' | 'admin' | 'mfa'; -export type ResourceRelationshipType = - | 'runs_on' - | 'depends_on' - | 'mounted_to' - | 'exposed_by' - | 'owned_by'; export type ResourceChangeConfidence = 'high' | 'medium' | 'low'; export type ResourceChangeKind = | 'state_transition' @@ -173,26 +166,6 @@ export type ResourceFacetSourceAdapter = | 'truenas_adapter' | 'agent:ops-helper'; -export interface ResourceCapabilityParam { - name: string; - type: string; - required: boolean; - enum?: string[]; - pattern?: string; - defaultValue?: unknown; - isSensitive: boolean; - description?: string; -} - -export interface ResourceCapability { - name: string; - type: ResourceCapabilityType; - description: string; - minimumApprovalLevel: ResourceApprovalLevel; - platform?: string; - params?: ResourceCapabilityParam[]; -} - export interface ResourceFacetCounts { recentChanges: number; recentChangeKinds?: Partial>; @@ -200,18 +173,6 @@ export interface ResourceFacetCounts { recentChangeSourceAdapters?: Partial>; } -export interface ResourceRelationship { - sourceId: string; - targetId: string; - type: ResourceRelationshipType; - confidence: number; - active: boolean; - discoverer: string; - observedAt: string; - lastSeenAt: string; - metadata?: Record; -} - export interface ResourceChange { id: string; observedAt: string; diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 661f94424..fde9ec14c 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -73,7 +73,6 @@ import dashboardEmptyStatePresentationSource from '@/utils/dashboardEmptyStatePr import throughputPresentationSource from '@/utils/throughputPresentation.ts?raw'; import resourceChangeSummarySource from '@/components/Infrastructure/ResourceChangeSummary.tsx?raw'; import resourceChangePresentationSource from '@/utils/resourceChangePresentation.ts?raw'; -import resourceRelationshipPresentationSource from '@/utils/resourceRelationshipPresentation.ts?raw'; import resourceCorrelationPresentationSource from '@/utils/resourceCorrelationPresentation.ts?raw'; import confidencePresentationSource from '@/utils/confidencePresentation.ts?raw'; import approvalPresentationSource from '@/utils/approvalPresentation.ts?raw'; @@ -1914,7 +1913,6 @@ describe('frontend resource type boundaries', () => { expect(resourceChangePresentationSource).toContain( 'getResourceChangeSourceAdapterPresentation', ); - expect(resourceRelationshipPresentationSource).toContain('formatConfidencePercentage'); expect(resourceCorrelationPresentationSource).toContain('formatConfidencePercentage'); expect(resourceCorrelationPresentationSource).toContain('humanizeArrowDelimitedLabel'); expect(resourceCorrelationPresentationSource).toContain('asTrimmedString'); @@ -1924,13 +1922,11 @@ describe('frontend resource type boundaries', () => { expect(throughputPresentationSource).toContain('formatThroughputRate'); expect(resourceDetailDrawerSource).toContain('formatIdentifierLabel'); expect(resourceChangePresentationSource).toContain('humanizeToken'); - expect(resourceRelationshipPresentationSource).toContain('humanizeToken'); expect(textPresentationSource).toContain('humanizeArrowDelimitedLabel'); expect(resourceCorrelationPresentationSource).not.toContain('formatResourceCorrelationEndpointLabel'); expect(resourceCorrelationPresentationSource).not.toContain( "replace(/\\s*->\\s*/g, ' → ')", ); - expect(resourceDetailDrawerSource).toContain('humanizeToken'); expect(textPresentationSource).toContain('humanizeToken'); expect(textPresentationSource).toContain('formatIdentifierLabel'); expect(resourceCorrelationPresentationSource).not.toContain('formatTrimmedLabel'); diff --git a/frontend-modern/src/utils/__tests__/resourceRelationshipPresentation.test.ts b/frontend-modern/src/utils/__tests__/resourceRelationshipPresentation.test.ts deleted file mode 100644 index 610b4e817..000000000 --- a/frontend-modern/src/utils/__tests__/resourceRelationshipPresentation.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { describeResourceRelationship, formatResourceRelationshipType } from '@/utils/resourceRelationshipPresentation'; - -describe('resourceRelationshipPresentation', () => { - it('formats canonical relationship type labels', () => { - expect(formatResourceRelationshipType('runs_on')).toBe('Runs on'); - expect(formatResourceRelationshipType('depends_on')).toBe('Depends on'); - expect(formatResourceRelationshipType('mounted_to')).toBe('Mounted to'); - expect(formatResourceRelationshipType('exposed_by')).toBe('Exposed by'); - expect(formatResourceRelationshipType('owned_by')).toBe('Owned by'); - expect(formatResourceRelationshipType('custom_link')).toBe('Custom Link'); - expect(formatResourceRelationshipType('')).toBe('Related to'); - }); - - it('describes canonical relationship context fragments', () => { - const presentation = describeResourceRelationship({ - sourceId: 'node-1', - targetId: 'vm-1', - type: 'runs_on', - confidence: 0.85, - active: false, - discoverer: 'proxmox_adapter', - observedAt: '2026-03-18T12:00:00Z', - lastSeenAt: '2026-03-18T12:05:00Z', - metadata: { - region: 'lab', - }, - }); - - expect(presentation).toMatchObject({ - typeLabel: 'Runs on', - direction: 'node-1 → vm-1', - provenance: 'proxmox_adapter', - stateLabel: 'Historical', - confidence: '85%', - hasMetadata: true, - }); - }); -}); diff --git a/frontend-modern/src/utils/resourceRelationshipPresentation.ts b/frontend-modern/src/utils/resourceRelationshipPresentation.ts deleted file mode 100644 index e4f6f6f21..000000000 --- a/frontend-modern/src/utils/resourceRelationshipPresentation.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { ResourceRelationship, ResourceRelationshipType } from '@/types/resource'; -import { formatConfidencePercentage } from '@/utils/confidencePresentation'; -import { humanizeToken } from '@/utils/textPresentation'; - -export interface ResourceRelationshipPresentation { - typeLabel: string; - direction: string; - provenance: string; - stateLabel: string; - confidence: string; - hasMetadata: boolean; -} - -export function formatResourceRelationshipType(type: ResourceRelationshipType | string): string { - switch (type) { - case 'runs_on': - return 'Runs on'; - case 'depends_on': - return 'Depends on'; - case 'mounted_to': - return 'Mounted to'; - case 'exposed_by': - return 'Exposed by'; - case 'owned_by': - return 'Owned by'; - default: { - return humanizeToken(String(type), { fallback: 'Related to' }); - } - } -} - -export function describeResourceRelationship( - relationship: ResourceRelationship, -): ResourceRelationshipPresentation { - return { - typeLabel: formatResourceRelationshipType(relationship.type), - direction: `${relationship.sourceId.trim()} → ${relationship.targetId.trim()}`, - provenance: relationship.discoverer.trim(), - stateLabel: relationship.active ? 'Active' : 'Historical', - confidence: relationship.confidence > 0 ? formatConfidencePercentage(relationship.confidence) : '', - hasMetadata: Boolean(relationship.metadata && Object.keys(relationship.metadata).length > 0), - }; -} diff --git a/internal/ai/chat/service.go b/internal/ai/chat/service.go index 334cf677e..0b3bb1276 100644 --- a/internal/ai/chat/service.go +++ b/internal/ai/chat/service.go @@ -51,7 +51,6 @@ type ( MetadataUpdater = tools.MetadataUpdater IncidentRecorderProvider = tools.IncidentRecorderProvider EventCorrelatorProvider = tools.EventCorrelatorProvider - TopologyProvider = tools.TopologyProvider KnowledgeStoreProvider = tools.KnowledgeStoreProvider MCPDiscoveryProvider = tools.DiscoveryProvider MCPUnifiedResourceProvider = tools.UnifiedResourceProvider @@ -1218,14 +1217,6 @@ func (s *Service) SetEventCorrelatorProvider(provider EventCorrelatorProvider) { } } -func (s *Service) SetTopologyProvider(provider TopologyProvider) { - s.mu.Lock() - defer s.mu.Unlock() - if s.executor != nil { - s.executor.SetTopologyProvider(provider) - } -} - func (s *Service) SetKnowledgeStoreProvider(provider KnowledgeStoreProvider) { s.mu.Lock() defer s.mu.Unlock() diff --git a/internal/ai/chat/service_additional_test.go b/internal/ai/chat/service_additional_test.go index 9790aeb96..f364ff7cc 100644 --- a/internal/ai/chat/service_additional_test.go +++ b/internal/ai/chat/service_additional_test.go @@ -17,7 +17,6 @@ func TestServiceSettersAndAutonomousMode(t *testing.T) { service.SetIncidentRecorderProvider(nil) service.SetEventCorrelatorProvider(nil) - service.SetTopologyProvider(nil) service.SetKnowledgeStoreProvider(nil) service.SetAutonomousMode(true) diff --git a/internal/ai/tools/executor.go b/internal/ai/tools/executor.go index 86115333c..b8c745965 100644 --- a/internal/ai/tools/executor.go +++ b/internal/ai/tools/executor.go @@ -362,7 +362,6 @@ type ExecutorConfig struct { // Optional providers - intelligence IncidentRecorderProvider IncidentRecorderProvider EventCorrelatorProvider EventCorrelatorProvider - TopologyProvider TopologyProvider KnowledgeStoreProvider KnowledgeStoreProvider // Optional providers - discovery @@ -414,7 +413,6 @@ type PulseToolExecutor struct { // Intelligence providers incidentRecorderProvider IncidentRecorderProvider eventCorrelatorProvider EventCorrelatorProvider - topologyProvider TopologyProvider knowledgeStoreProvider KnowledgeStoreProvider // Discovery provider @@ -493,7 +491,6 @@ func NewPulseToolExecutor(cfg ExecutorConfig) *PulseToolExecutor { agentProfileManager: cfg.AgentProfileManager, incidentRecorderProvider: cfg.IncidentRecorderProvider, eventCorrelatorProvider: cfg.EventCorrelatorProvider, - topologyProvider: cfg.TopologyProvider, knowledgeStoreProvider: cfg.KnowledgeStoreProvider, discoveryProvider: cfg.DiscoveryProvider, unifiedResourceProvider: cfg.UnifiedResourceProvider, @@ -684,11 +681,6 @@ func (e *PulseToolExecutor) SetEventCorrelatorProvider(provider EventCorrelatorP e.eventCorrelatorProvider = provider } -// SetTopologyProvider sets the topology provider for relationship graphs -func (e *PulseToolExecutor) SetTopologyProvider(provider TopologyProvider) { - e.topologyProvider = provider -} - // SetKnowledgeStoreProvider sets the knowledge store provider for notes func (e *PulseToolExecutor) SetKnowledgeStoreProvider(provider KnowledgeStoreProvider) { e.knowledgeStoreProvider = provider @@ -785,7 +777,7 @@ func (e *PulseToolExecutor) isToolAvailable(name string) bool { case "pulse_discovery": return e.discoveryProvider != nil case "pulse_knowledge": - return e.knowledgeStoreProvider != nil || e.incidentRecorderProvider != nil || e.eventCorrelatorProvider != nil || e.topologyProvider != nil + return e.knowledgeStoreProvider != nil || e.incidentRecorderProvider != nil || e.eventCorrelatorProvider != nil case "pulse_pmg": return e.hasReadState() case "patrol_report_finding", "patrol_resolve_finding", "patrol_get_findings": diff --git a/internal/ai/tools/tools_knowledge.go b/internal/ai/tools/tools_knowledge.go index 896884e0d..614616611 100644 --- a/internal/ai/tools/tools_knowledge.go +++ b/internal/ai/tools/tools_knowledge.go @@ -59,19 +59,6 @@ type EventCorrelation struct { Metadata map[string]interface{} `json:"metadata,omitempty"` } -// TopologyProvider provides access to resource relationships -type TopologyProvider interface { - GetRelatedResources(resourceID string, depth int) []RelatedResource -} - -// RelatedResource represents a resource related to another resource -type RelatedResource struct { - ResourceID string `json:"resource_id"` - ResourceName string `json:"resource_name"` - ResourceType string `json:"resource_type"` - Relationship string `json:"relationship"` -} - // KnowledgeStoreProvider provides access to stored knowledge/notes type KnowledgeStoreProvider interface { SaveNote(resourceID, note, category string) error @@ -100,21 +87,19 @@ Actions: - recall: Retrieve saved notes about a resource - incidents: Get high-resolution incident recording data - correlate: Get correlated events around a timestamp -- relationships: Get resource dependency graph Examples: - Save note: action="remember", resource_id="101", note="Production database server", category="purpose" - Recall: action="recall", resource_id="101" - Get incidents: action="incidents", resource_id="101" -- Correlate events: action="correlate", resource_id="101", window_minutes=30 -- Get relationships: action="relationships", resource_id="101"`, +- Correlate events: action="correlate", resource_id="101", window_minutes=30`, InputSchema: InputSchema{ Type: "object", Properties: map[string]PropertySchema{ "action": { Type: "string", Description: "Knowledge action to perform", - Enum: []string{"remember", "recall", "incidents", "correlate", "relationships"}, + Enum: []string{"remember", "recall", "incidents", "correlate"}, }, "resource_id": { Type: "string", @@ -140,10 +125,6 @@ Examples: Type: "integer", Description: "For correlate: time window in minutes (default: 15)", }, - "depth": { - Type: "integer", - Description: "For relationships: levels to traverse (default: 1, max: 3)", - }, "limit": { Type: "integer", Description: "For incidents: max windows to return (default: 5)", @@ -170,10 +151,8 @@ func (e *PulseToolExecutor) executeKnowledge(ctx context.Context, args map[strin return e.executeGetIncidentWindow(ctx, args) case "correlate": return e.executeCorrelateEvents(ctx, args) - case "relationships": - return e.executeGetRelationshipGraph(ctx, args) default: - return NewErrorResult(fmt.Errorf("unknown action: %s. Use: remember, recall, incidents, correlate, relationships", action)), nil + return NewErrorResult(fmt.Errorf("unknown action: %s. Use: remember, recall, incidents, correlate", action)), nil } } @@ -258,39 +237,6 @@ func (e *PulseToolExecutor) executeCorrelateEvents(_ context.Context, args map[s }), nil } -func (e *PulseToolExecutor) executeGetRelationshipGraph(_ context.Context, args map[string]interface{}) (CallToolResult, error) { - resourceID, _ := args["resource_id"].(string) - depth := intArg(args, "depth", 1) - - if resourceID == "" { - return NewErrorResult(fmt.Errorf("resource_id is required")), nil - } - - // Cap depth to prevent excessive traversal - if depth < 1 { - depth = 1 - } - if depth > 3 { - depth = 3 - } - - if e.topologyProvider == nil { - return NewTextResult("Topology information not available."), nil - } - - related := e.topologyProvider.GetRelatedResources(resourceID, depth) - if len(related) == 0 { - return NewTextResult(fmt.Sprintf("No relationships found for resource '%s'.", resourceID)), nil - } - - return NewJSONResult(map[string]interface{}{ - "resource_id": resourceID, - "depth": depth, - "related_resources": related, - "count": len(related), - }), nil -} - func (e *PulseToolExecutor) executeRemember(_ context.Context, args map[string]interface{}) (CallToolResult, error) { resourceID, _ := args["resource_id"].(string) note, _ := args["note"].(string) diff --git a/internal/api/ai_handler.go b/internal/api/ai_handler.go index a9bc5b6b3..0dca1b09f 100644 --- a/internal/api/ai_handler.go +++ b/internal/api/ai_handler.go @@ -64,7 +64,6 @@ type AIService interface { SetKnowledgeStoreProvider(provider chat.KnowledgeStoreProvider) SetIncidentRecorderProvider(provider chat.IncidentRecorderProvider) SetEventCorrelatorProvider(provider chat.EventCorrelatorProvider) - SetTopologyProvider(provider chat.TopologyProvider) SetDiscoveryProvider(provider chat.MCPDiscoveryProvider) SetUnifiedResourceProvider(provider chat.MCPUnifiedResourceProvider) UpdateControlSettings(cfg *config.AIConfig) diff --git a/internal/api/ai_handler_test.go b/internal/api/ai_handler_test.go index 613dfb315..5e47a2a14 100644 --- a/internal/api/ai_handler_test.go +++ b/internal/api/ai_handler_test.go @@ -146,7 +146,6 @@ func (m *MockAIService) SetIncidentRecorderProvider(provider chat.IncidentRecord func (m *MockAIService) SetEventCorrelatorProvider(provider chat.EventCorrelatorProvider) { m.Called(provider) } -func (m *MockAIService) SetTopologyProvider(provider chat.TopologyProvider) { m.Called(provider) } func (m *MockAIService) SetDiscoveryProvider(provider chat.MCPDiscoveryProvider) { m.Called(provider) } diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index 6ea3aa457..3a1f9b7ba 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -3810,8 +3810,6 @@ func TestContract_ResourceListCarriesTimelineAndCapabilityContracts(t *testing.T }, }, FacetCounts: unifiedresources.ResourceFacetCounts{ - Capabilities: 1, - Relationships: 1, RecentChanges: 1, }, }, @@ -3894,8 +3892,6 @@ func TestContract_ResourceListCarriesTimelineAndCapabilityContracts(t *testing.T } ], "facetCounts":{ - "capabilities":1, - "relationships":1, "recentChanges":1 } } @@ -4258,13 +4254,9 @@ func TestContract_ResourceTimelineAnomalyJSONSnapshot(t *testing.T) { func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 17, 0, 0, 0, time.UTC) payload := struct { - ResourceID string `json:"resourceId"` - Capabilities []unifiedresources.ResourceCapability `json:"capabilities"` - Relationships []unifiedresources.ResourceRelationship `json:"relationships"` - RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"` + ResourceID string `json:"resourceId"` + RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"` Counts struct { - Capabilities int `json:"capabilities"` - Relationships int `json:"relationships"` RecentChanges int `json:"recentChanges"` RecentChangeKinds map[unifiedresources.ChangeKind]int `json:"recentChangeKinds"` RecentChangeSourceTypes map[unifiedresources.ChangeSourceType]int `json:"recentChangeSourceTypes"` @@ -4272,30 +4264,6 @@ func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) { } `json:"counts"` }{ ResourceID: "vm:42", - Capabilities: []unifiedresources.ResourceCapability{ - { - Name: "restart", - Type: unifiedresources.CapabilityTypeCommon, - Description: "Restart the VM", - MinimumApprovalLevel: unifiedresources.ApprovalAdmin, - }, - }, - Relationships: []unifiedresources.ResourceRelationship{ - { - SourceID: "vm:42", - TargetID: "node-1", - Type: unifiedresources.RelRunsOn, - Confidence: 1, - Active: true, - Discoverer: "proxmox_adapter", - ObservedAt: now, - LastSeenAt: now, - Metadata: map[string]any{ - "source": "live", - "cluster": "pve-prod", - }, - }, - }, RecentChanges: []unifiedresources.ResourceChange{ { ID: "chg-42", @@ -4316,15 +4284,11 @@ func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) { }, }, Counts: struct { - Capabilities int `json:"capabilities"` - Relationships int `json:"relationships"` RecentChanges int `json:"recentChanges"` RecentChangeKinds map[unifiedresources.ChangeKind]int `json:"recentChangeKinds"` RecentChangeSourceTypes map[unifiedresources.ChangeSourceType]int `json:"recentChangeSourceTypes"` RecentChangeSourceAdapters map[unifiedresources.ChangeSourceAdapter]int `json:"recentChangeSourceAdapters"` }{ - Capabilities: 1, - Relationships: 1, RecentChanges: 3, RecentChangeKinds: map[unifiedresources.ChangeKind]int{unifiedresources.ChangeRestart: 1, unifiedresources.ChangeAnomaly: 2}, RecentChangeSourceTypes: map[unifiedresources.ChangeSourceType]int{ @@ -4345,27 +4309,6 @@ func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) { const want = `{ "resourceId":"vm:42", - "capabilities":[ - { - "name":"restart", - "type":"common", - "description":"Restart the VM", - "minimumApprovalLevel":"admin" - } - ], - "relationships":[ - { - "sourceId":"vm:42", - "targetId":"node-1", - "type":"runs_on", - "confidence":1, - "active":true, - "discoverer":"proxmox_adapter", - "observedAt":"2026-03-18T17:00:00Z", - "lastSeenAt":"2026-03-18T17:00:00Z", - "metadata":{"cluster":"pve-prod","source":"live"} - } - ], "recentChanges":[ { "id":"chg-42", @@ -4383,8 +4326,6 @@ func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) { } ], "counts":{ - "capabilities":1, - "relationships":1, "recentChanges":3, "recentChangeKinds":{ "metric_anomaly":2, diff --git a/internal/api/resources.go b/internal/api/resources.go index 2831e758a..5e8916994 100644 --- a/internal/api/resources.go +++ b/internal/api/resources.go @@ -278,11 +278,9 @@ func (h *ResourceHandlers) HandleGetResource(w http.ResponseWriter, r *http.Requ type resourceFacetCountsResponse = unified.ResourceFacetCounts type resourceFacetBundleResponse struct { - ResourceID string `json:"resourceId"` - Capabilities []unified.ResourceCapability `json:"capabilities"` - Relationships []unified.ResourceRelationship `json:"relationships"` - RecentChanges []unified.ResourceChange `json:"recentChanges"` - Counts resourceFacetCountsResponse `json:"counts"` + ResourceID string `json:"resourceId"` + RecentChanges []unified.ResourceChange `json:"recentChanges"` + Counts resourceFacetCountsResponse `json:"counts"` } // HandleResourceRoutes dispatches nested resource routes. @@ -346,7 +344,7 @@ func (h *ResourceHandlers) HandleGetResourceFacets(w http.ResponseWriter, r *htt return } - resource, ok := registry.Get(resourceID) + _, ok := registry.Get(resourceID) if !ok { http.Error(w, "Resource not found", http.StatusNotFound) return @@ -402,24 +400,11 @@ func (h *ResourceHandlers) HandleGetResourceFacets(w http.ResponseWriter, r *htt return } - capabilities := resource.Capabilities - if capabilities == nil { - capabilities = []unified.ResourceCapability{} - } - relationships := resource.Relationships - if relationships == nil { - relationships = []unified.ResourceRelationship{} - } - w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resourceFacetBundleResponse{ ResourceID: resourceID, - Capabilities: capabilities, - Relationships: relationships, RecentChanges: recentChanges, Counts: resourceFacetCountsResponse{ - Capabilities: resource.FacetCounts.Capabilities, - Relationships: resource.FacetCounts.Relationships, RecentChanges: changeCount, RecentChangeKinds: changeKindCounts, RecentChangeSourceTypes: sourceTypeCounts, diff --git a/internal/api/resources_test.go b/internal/api/resources_test.go index 2838f1a0a..259d531cc 100644 --- a/internal/api/resources_test.go +++ b/internal/api/resources_test.go @@ -246,8 +246,8 @@ func TestResourceListUsesUnifiedSeedProvider(t *testing.T) { if strings.TrimSpace(resp.Data[0].AISafeSummary) == "" { t.Fatal("expected aiSafeSummary on seeded resource") } - if got := resp.Data[0].FacetCounts; got.Capabilities != 1 || got.Relationships != 1 || got.RecentChanges != 1 { - t.Fatalf("facetCounts = %+v, want 1/1/1", got) + if got := resp.Data[0].FacetCounts; got.RecentChanges != 1 { + t.Fatalf("facetCounts = %+v, want recentChanges=1", got) } } @@ -981,13 +981,9 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) { t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) } var payload struct { - ResourceID string `json:"resourceId"` - Capabilities []unified.ResourceCapability `json:"capabilities"` - Relationships []unified.ResourceRelationship `json:"relationships"` - RecentChanges []unified.ResourceChange `json:"recentChanges"` + ResourceID string `json:"resourceId"` + RecentChanges []unified.ResourceChange `json:"recentChanges"` Counts struct { - Capabilities int `json:"capabilities"` - Relationships int `json:"relationships"` RecentChanges int `json:"recentChanges"` RecentChangeKinds map[unified.ChangeKind]int `json:"recentChangeKinds"` RecentChangeSourceTypes map[unified.ChangeSourceType]int `json:"recentChangeSourceTypes"` @@ -1000,9 +996,6 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) { if payload.ResourceID != "vm:42" || payload.Counts.RecentChanges != 3 || len(payload.RecentChanges) != 1 { t.Fatalf("unexpected facets payload: %#v", payload) } - if payload.Counts.Capabilities != 1 || payload.Counts.Relationships != 1 { - t.Fatalf("unexpected facet counts: %#v", payload.Counts) - } if got := payload.Counts.RecentChangeKinds; len(got) != 2 || got[unified.ChangeRestart] != 1 || got[unified.ChangeAnomaly] != 2 { t.Fatalf("unexpected recent change kind counts: %#v", got) } @@ -1012,9 +1005,6 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) { if got := payload.Counts.RecentChangeSourceAdapters; len(got) != 2 || got[unified.AdapterProxmox] != 2 || got[unified.AdapterDocker] != 1 { t.Fatalf("unexpected recent change source adapter counts: %#v", got) } - if got := payload.Relationships[0].Metadata["cluster"]; got != "pve-prod" { - t.Fatalf("unexpected relationship metadata: %#v", payload.Relationships[0].Metadata) - } if got := payload.RecentChanges[0].Metadata["ticket"]; got != "INC-1234" { t.Fatalf("unexpected change metadata: %#v", payload.RecentChanges[0].Metadata) } @@ -1106,12 +1096,9 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) { t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) } var payload struct { - ResourceID string `json:"resourceId"` - RecentChanges []unified.ResourceChange `json:"recentChanges"` - Relationships []unified.ResourceRelationship `json:"relationships"` + ResourceID string `json:"resourceId"` + RecentChanges []unified.ResourceChange `json:"recentChanges"` Counts struct { - Capabilities int `json:"capabilities"` - Relationships int `json:"relationships"` RecentChanges int `json:"recentChanges"` RecentChangeKinds map[unified.ChangeKind]int `json:"recentChangeKinds"` RecentChangeSourceTypes map[unified.ChangeSourceType]int `json:"recentChangeSourceTypes"` @@ -1146,12 +1133,9 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) { t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) } var payload struct { - ResourceID string `json:"resourceId"` - RecentChanges []unified.ResourceChange `json:"recentChanges"` - Relationships []unified.ResourceRelationship `json:"relationships"` + ResourceID string `json:"resourceId"` + RecentChanges []unified.ResourceChange `json:"recentChanges"` Counts struct { - Capabilities int `json:"capabilities"` - Relationships int `json:"relationships"` RecentChanges int `json:"recentChanges"` RecentChangeKinds map[unified.ChangeKind]int `json:"recentChangeKinds"` RecentChangeSourceTypes map[unified.ChangeSourceType]int `json:"recentChangeSourceTypes"` diff --git a/internal/api/router.go b/internal/api/router.go index e75a3dc56..4f9525eb2 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -2229,7 +2229,6 @@ func (r *Router) wireAIChatDependenciesForService(ctx context.Context, service A // Wire intelligence providers for MCP tools // - IncidentRecorderProvider: high-frequency incident data (pulse_get_incident_window) // - EventCorrelatorProvider: Proxmox events (pulse_correlate_events) - // - TopologyProvider: relationship graph (pulse_get_relationship_graph) // - KnowledgeStoreProvider: notes (pulse_remember, pulse_recall) // Wire incident recorder provider (high-frequency incident data) diff --git a/internal/unifiedresources/clone.go b/internal/unifiedresources/clone.go index 86f5f851a..187f09319 100644 --- a/internal/unifiedresources/clone.go +++ b/internal/unifiedresources/clone.go @@ -47,8 +47,6 @@ func cloneResource(in *Resource) Resource { func resourceFacetCounts(resource Resource) ResourceFacetCounts { return ResourceFacetCounts{ - Capabilities: len(resource.Capabilities), - Relationships: len(resource.Relationships), RecentChanges: len(resource.RecentChanges), } } diff --git a/internal/unifiedresources/code_standards_test.go b/internal/unifiedresources/code_standards_test.go index 8ea6e1235..93291b102 100644 --- a/internal/unifiedresources/code_standards_test.go +++ b/internal/unifiedresources/code_standards_test.go @@ -804,6 +804,39 @@ func TestResourceFacetCountsAreCanonicalResourceFields(t *testing.T) { } } +func TestCanonicalIdentityIsCanonicalResourceField(t *testing.T) { + typesData, err := os.ReadFile(filepath.Join("types.go")) + if err != nil { + t.Fatalf("failed to read types.go: %v", err) + } + typesSource := string(typesData) + requiredSnippets := []string{ + "json:\"canonicalIdentity,omitempty\"", + "type CanonicalIdentity struct {", + "DisplayName string `json:\"displayName,omitempty\"`", + "Aliases []string `json:\"aliases,omitempty\"`", + } + for _, snippet := range requiredSnippets { + if !strings.Contains(typesSource, snippet) { + t.Fatalf("internal/unifiedresources/types.go must keep the canonical identity contract snippet %q", snippet) + } + } + + cloneData, err := os.ReadFile(filepath.Join("clone.go")) + if err != nil { + t.Fatalf("failed to read clone.go: %v", err) + } + cloneSource := string(cloneData) + requiredCloneSnippets := []string{ + "RefreshCanonicalMetadata(&out)", + } + for _, snippet := range requiredCloneSnippets { + if !strings.Contains(cloneSource, snippet) { + t.Fatalf("internal/unifiedresources/clone.go must preserve canonical identity via %q", snippet) + } + } +} + // TestNoLegacyHostResourceTypeSymbol prevents reintroducing the removed // ResourceTypeHost symbol. v6 code must use ResourceTypeAgent and // CanonicalResourceType() for legacy normalization. diff --git a/internal/unifiedresources/types.go b/internal/unifiedresources/types.go index 169a3af10..565c1663f 100644 --- a/internal/unifiedresources/types.go +++ b/internal/unifiedresources/types.go @@ -69,8 +69,6 @@ type Resource struct { // ResourceFacetCounts captures the total count of each resource facet that // may be surfaced in row summaries or detail drawers. type ResourceFacetCounts struct { - Capabilities int `json:"capabilities"` - Relationships int `json:"relationships"` RecentChanges int `json:"recentChanges"` RecentChangeKinds map[ChangeKind]int `json:"recentChangeKinds,omitempty"` RecentChangeSourceTypes map[ChangeSourceType]int `json:"recentChangeSourceTypes,omitempty"`