mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Clarify canonical timeline ownership
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Pulse v6 Source Of Truth
|
||||
|
||||
Last updated: 2026-03-17
|
||||
Last updated: 2026-03-20
|
||||
Status: ACTIVE
|
||||
|
||||
This file is the stable human governance layer for the active v6 release
|
||||
@@ -398,6 +398,11 @@ Assertion design rules:
|
||||
defensible stop point and complete the next obvious same-lane work when it
|
||||
is necessary for trustworthy results, then normalize any remaining valid
|
||||
gap instead of calling the lane done by default.
|
||||
12. Unified-resource change history is the canonical durable backend timeline.
|
||||
Alert incident memory may retain investigation-local notes, analysis,
|
||||
commands, runbooks, and alert lifecycle breadcrumbs, but it must remain a
|
||||
derived incident projection rather than a competing source of truth for
|
||||
durable resource history.
|
||||
|
||||
## Cross-Repo Contracts
|
||||
|
||||
|
||||
@@ -188,6 +188,11 @@ detectors only serving as fallback coverage when the canonical store is not
|
||||
available. When that patrol-local fallback is used, it must render through the
|
||||
shared memory change presentation helper so the same heading, scope prefix, and
|
||||
change-type labels are reused instead of being rebuilt ad hoc in AI-local code.
|
||||
`internal/ai/memory/incidents.go` is therefore an alert-scoped investigation
|
||||
projection only: it may retain notes, analysis, command executions, runbooks,
|
||||
and alert lifecycle breadcrumbs for one incident, but it must not become a
|
||||
parallel source of truth for durable backend history that already belongs to
|
||||
`internal/unifiedresources/`.
|
||||
The AI correlation root-cause engine also consumes the canonical unified-
|
||||
resource relationship model directly, so cross-resource reasoning stays aligned
|
||||
with the same relationship edges that back the resource API instead of
|
||||
@@ -246,6 +251,9 @@ Resource-only incident context should follow the same rule: if an alert
|
||||
timeline is absent, the incident prompt path should fall back to the canonical
|
||||
unified-resource timeline rather than depending only on patrol-local change
|
||||
memory.
|
||||
When both an alert identifier and a canonical resource ID are known, the prompt
|
||||
path should include both surfaces in source-precedence order: alert-scoped
|
||||
incident memory first, canonical resource timeline second.
|
||||
|
||||
The same runtime boundary now also owns durable action execution auditing.
|
||||
`internal/ai/chat/service.go` initializes the unified-resource audit store on
|
||||
|
||||
@@ -463,6 +463,13 @@ record state that SQLite persists for the control-plane execution trail.
|
||||
The enterprise audit API now reads those same unified-resource action and
|
||||
export records back out, so the durable store is not just a write sink but the
|
||||
canonical history surface for the control-plane verbs.
|
||||
That same ownership boundary applies to incident-adjacent runtime history:
|
||||
durable backend facts about what changed on a resource belong in
|
||||
`ResourceChange` and the shared unified-resource store, while
|
||||
`internal/ai/memory/incidents.go` remains an alert-scoped investigation
|
||||
projection for notes, analyses, command breadcrumbs, runbooks, and other
|
||||
operator-facing incident memory. Agents must not model the same durable backend
|
||||
fact in both places as competing primary histories.
|
||||
|
||||
The unified resource core is strong and canonical, but monitoring and some
|
||||
frontend/API consumers are still being tightened around it.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/baseline"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/correlation"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/memory"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
)
|
||||
@@ -479,6 +480,55 @@ func TestService_BuildIncidentContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_BuildIncidentContext_AlertIncludesCanonicalResourceChanges(t *testing.T) {
|
||||
s := NewService(nil, nil)
|
||||
|
||||
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{})
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-canonical-1",
|
||||
Type: "cpu_high",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-canonical-1",
|
||||
ResourceName: "vm-canonical-1",
|
||||
StartTime: time.Now().Add(-30 * time.Minute),
|
||||
}
|
||||
incidentStore.RecordAlertFired(alert)
|
||||
|
||||
canonicalStore := unifiedresources.NewMemoryStore()
|
||||
if err := canonicalStore.RecordChange(unifiedresources.ResourceChange{
|
||||
ID: "change-canonical-1",
|
||||
ObservedAt: time.Now().Add(-15 * time.Minute),
|
||||
ResourceID: alert.ResourceID,
|
||||
Kind: unifiedresources.ChangeConfigUpdate,
|
||||
From: "old",
|
||||
To: "new",
|
||||
SourceType: unifiedresources.SourcePulseDiff,
|
||||
SourceAdapter: unifiedresources.AdapterOpsAgent,
|
||||
Confidence: unifiedresources.ConfidenceHigh,
|
||||
Reason: "Config drift corrected",
|
||||
}); err != nil {
|
||||
t.Fatalf("record canonical change: %v", err)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.incidentStore = incidentStore
|
||||
s.orgID = "org-1"
|
||||
s.resourceExportStore = canonicalStore
|
||||
s.resourceExportStoreOrgID = s.orgID
|
||||
s.mu.Unlock()
|
||||
|
||||
ctx := s.buildIncidentContext(alert.ResourceID, alert.ID)
|
||||
if !strings.Contains(ctx, "Incident Memory") {
|
||||
t.Fatalf("expected alert incident memory in context, got %q", ctx)
|
||||
}
|
||||
if !strings.Contains(ctx, "Recent Changes") {
|
||||
t.Fatalf("expected canonical recent changes in alert context, got %q", ctx)
|
||||
}
|
||||
if !strings.Contains(ctx, "Config update") {
|
||||
t.Fatalf("expected canonical change summary in alert context, got %q", ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_BuildRelationshipContext_UsesCanonicalReadState(t *testing.T) {
|
||||
now := time.Now()
|
||||
s := NewService(nil, nil)
|
||||
|
||||
@@ -45,7 +45,9 @@ type IncidentEvent struct {
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// Incident captures an alert occurrence and its timeline.
|
||||
// Incident captures an alert occurrence and its investigation timeline.
|
||||
// It is an alert-scoped memory/projection for investigation support rather than
|
||||
// the canonical durable resource-change history.
|
||||
type Incident struct {
|
||||
ID string `json:"id"`
|
||||
AlertIdentifier string `json:"alertIdentifier"`
|
||||
@@ -147,7 +149,9 @@ type IncidentStoreConfig struct {
|
||||
MaxAgeDays int
|
||||
}
|
||||
|
||||
// IncidentStore maintains incident timelines and persistence.
|
||||
// IncidentStore maintains alert-scoped incident timelines and persistence for
|
||||
// investigation memory. Durable resource history belongs to the canonical
|
||||
// unified-resource change model.
|
||||
type IncidentStore struct {
|
||||
mu sync.RWMutex
|
||||
saveMu sync.Mutex
|
||||
|
||||
@@ -183,7 +183,7 @@ type Service struct {
|
||||
resourceExportStoreOrgID string
|
||||
patrolService *PatrolService // Background AI monitoring service
|
||||
metadataProvider MetadataProvider // Enables AI to update resource URLs
|
||||
incidentStore *memory.IncidentStore // Incident timelines for alert memory
|
||||
incidentStore *memory.IncidentStore // Alert-scoped investigation memory; not the canonical durable resource history
|
||||
chatService ChatServiceProvider // Chat service for investigation orchestrator
|
||||
|
||||
// Infrastructure discovery service - detects apps running on hosts
|
||||
@@ -4488,7 +4488,10 @@ func (s *Service) buildRemediationContext(resourceID, currentProblem string) str
|
||||
return context
|
||||
}
|
||||
|
||||
// buildIncidentContext adds incident timeline context for alerts/resources.
|
||||
// buildIncidentContext adds alert-scoped incident memory first, then the
|
||||
// canonical resource timeline and relationship context when a resource is known.
|
||||
// The incident store is a derived investigation view, not the source of truth
|
||||
// for durable backend history.
|
||||
func (s *Service) buildIncidentContext(resourceID, alertIdentifier string) string {
|
||||
s.mu.RLock()
|
||||
store := s.incidentStore
|
||||
@@ -4508,7 +4511,7 @@ func (s *Service) buildIncidentContext(resourceID, alertIdentifier string) strin
|
||||
}
|
||||
}
|
||||
|
||||
if alertIdentifier == "" && resourceID != "" {
|
||||
if resourceID != "" {
|
||||
if recentChanges := s.buildRecentResourceChangesContext(resourceID); recentChanges != "" {
|
||||
sections = append(sections, recentChanges)
|
||||
}
|
||||
|
||||
@@ -2047,7 +2047,10 @@ class SubsystemLookupTest(unittest.TestCase):
|
||||
self.assertEqual(lane_context["lane_id"], "L13")
|
||||
self.assertEqual(
|
||||
{decision["id"] for decision in lane_context["resolved_decisions"]},
|
||||
{"host-type-migration-boundary-audit"},
|
||||
{
|
||||
"canonical-timeline-source-precedence",
|
||||
"host-type-migration-boundary-audit",
|
||||
},
|
||||
)
|
||||
|
||||
def test_lookup_paths_assigns_monitoring_discovery_runtime(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user