Pulse/internal/api/telemetry_pulse_intelligence.go
2026-07-02 11:50:03 +01:00

287 lines
9.2 KiB
Go

package api
import (
"strings"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/telemetry"
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rs/zerolog/log"
)
// ApplyUpdateTelemetrySnapshot adds router-owned, content-free update funnel
// counters to the outbound usage telemetry snapshot.
func (r *Router) ApplyUpdateTelemetrySnapshot(s *telemetry.Snapshot, now time.Time) {
if r == nil || s == nil {
return
}
telemetry.ApplyUpdateTelemetrySnapshot(s, r.updateHistory, now)
}
// GetPulseIntelligenceActionTelemetry returns count-only action-governance
// telemetry for the outbound Pulse Intelligence usage loop. It deliberately drops
// command text, approval actors/reasons, action outputs, and resource IDs.
func (r *Router) GetPulseIntelligenceActionTelemetry(since time.Time) telemetry.PulseIntelligenceActionSnapshot {
var snapshot telemetry.PulseIntelligenceActionSnapshot
if r == nil || r.resourceHandlers == nil {
return snapshot
}
for _, orgID := range r.pulseIntelligenceTelemetryOrgIDs() {
store, err := r.resourceHandlers.getStore(orgID)
if err != nil || store == nil {
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to resolve action audit store for telemetry summary")
continue
}
approvedAttemptIDs, approvedSuccessIDs := pulseIntelligenceApprovedActionOutcomeIDs(store, orgID, since)
rejectedDecisionIDs := pulseIntelligenceRejectedActionDecisionIDs(store, orgID, since)
approvedDecisionIDs := pulseIntelligenceApprovedActionDecisionIDs(store, orgID, since)
records, err := store.GetActionAudits("", since, 0)
if err != nil {
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to query action audit telemetry summary")
continue
}
for _, record := range records {
snapshot.ActionPlans30d++
if pulseIntelligenceActionRequiresApproval(record) {
snapshot.ApprovalRequests30d++
}
if pulseIntelligenceActionWasRejected(record) {
if actionID := strings.TrimSpace(record.ID); actionID != "" {
rejectedDecisionIDs[actionID] = struct{}{}
}
}
if pulseIntelligenceActionWasApprovedSince(record, since) {
if actionID := strings.TrimSpace(record.ID); actionID != "" {
approvedDecisionIDs[actionID] = struct{}{}
}
}
if pulseIntelligenceApprovedActionAttempt(record) {
if actionID := strings.TrimSpace(record.ID); actionID != "" {
approvedAttemptIDs[actionID] = struct{}{}
}
}
if pulseIntelligenceApprovedActionSuccess(record) {
if actionID := strings.TrimSpace(record.ID); actionID != "" {
approvedSuccessIDs[actionID] = struct{}{}
}
}
}
snapshot.RejectedActionDecisions30d += len(rejectedDecisionIDs)
snapshot.ApprovedActionDecisions30d += len(approvedDecisionIDs)
snapshot.ApprovedActionAttempts30d += len(approvedAttemptIDs)
snapshot.ApprovedActionSuccesses30d += len(approvedSuccessIDs)
}
return snapshot
}
func (r *Router) pulseIntelligenceTelemetryOrgIDs() []string {
if r == nil || r.multiTenant == nil {
return []string{"default"}
}
orgs, err := r.multiTenant.ListOrganizations()
if err != nil {
log.Warn().Err(err).Msg("Unable to list organizations for telemetry summary")
return []string{"default"}
}
seen := map[string]struct{}{}
orgIDs := make([]string, 0, len(orgs))
for _, org := range orgs {
if org == nil {
continue
}
orgID := strings.TrimSpace(org.ID)
if orgID == "" {
orgID = "default"
}
if _, ok := seen[orgID]; ok {
continue
}
seen[orgID] = struct{}{}
orgIDs = append(orgIDs, orgID)
}
if len(orgIDs) == 0 {
return []string{"default"}
}
return orgIDs
}
func pulseIntelligenceActionRequiresApproval(record unifiedresources.ActionAuditRecord) bool {
if record.Plan.RequiresApproval {
return true
}
return len(record.Approvals) > 0 || record.State == unifiedresources.ActionStatePending ||
record.State == unifiedresources.ActionStateApproved || record.State == unifiedresources.ActionStateRejected
}
func pulseIntelligenceApprovedActionOutcomeIDs(store unifiedresources.ResourceStore, orgID string, since time.Time) (map[string]struct{}, map[string]struct{}) {
attemptIDs := map[string]struct{}{}
successIDs := map[string]struct{}{}
if store == nil {
return attemptIDs, successIDs
}
events, err := store.GetActionLifecycleEvents("", since, 0)
if err != nil {
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to query action lifecycle telemetry summary")
return attemptIDs, successIDs
}
for _, event := range events {
if !pulseIntelligenceActionLifecycleIndicatesAttempt(event) {
continue
}
actionID := strings.TrimSpace(event.ActionID)
if actionID == "" {
continue
}
record, ok, err := store.GetActionAudit(actionID)
if err != nil {
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to resolve action audit for lifecycle telemetry summary")
continue
}
if ok && pulseIntelligenceActionWasApproved(record) {
attemptIDs[actionID] = struct{}{}
if event.State == unifiedresources.ActionStateCompleted && pulseIntelligenceApprovedActionSuccess(record) {
successIDs[actionID] = struct{}{}
}
}
}
return attemptIDs, successIDs
}
func pulseIntelligenceRejectedActionDecisionIDs(store unifiedresources.ResourceStore, orgID string, since time.Time) map[string]struct{} {
decisionIDs := map[string]struct{}{}
if store == nil {
return decisionIDs
}
events, err := store.GetActionLifecycleEvents("", since, 0)
if err != nil {
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to query rejected action lifecycle telemetry summary")
return decisionIDs
}
for _, event := range events {
if event.State != unifiedresources.ActionStateRejected {
continue
}
actionID := strings.TrimSpace(event.ActionID)
if actionID == "" {
continue
}
record, ok, err := store.GetActionAudit(actionID)
if err != nil {
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to resolve rejected action audit for telemetry summary")
continue
}
if ok && pulseIntelligenceActionWasRejected(record) {
decisionIDs[actionID] = struct{}{}
}
}
return decisionIDs
}
func pulseIntelligenceApprovedActionDecisionIDs(store unifiedresources.ResourceStore, orgID string, since time.Time) map[string]struct{} {
decisionIDs := map[string]struct{}{}
if store == nil {
return decisionIDs
}
events, err := store.GetActionLifecycleEvents("", since, 0)
if err != nil {
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to query approved action lifecycle telemetry summary")
return decisionIDs
}
for _, event := range events {
if event.State != unifiedresources.ActionStateApproved {
continue
}
actionID := strings.TrimSpace(event.ActionID)
if actionID == "" {
continue
}
record, ok, err := store.GetActionAudit(actionID)
if err != nil {
log.Warn().Err(err).Str("org_id", orgID).Msg("Unable to resolve approved action audit for telemetry summary")
continue
}
if ok && pulseIntelligenceActionWasApproved(record) {
decisionIDs[actionID] = struct{}{}
}
}
return decisionIDs
}
func pulseIntelligenceApprovedActionAttempt(record unifiedresources.ActionAuditRecord) bool {
if !pulseIntelligenceActionWasApproved(record) {
return false
}
switch record.State {
case unifiedresources.ActionStateExecuting, unifiedresources.ActionStateCompleted, unifiedresources.ActionStateFailed:
return true
default:
return false
}
}
func pulseIntelligenceApprovedActionSuccess(record unifiedresources.ActionAuditRecord) bool {
if record.State != unifiedresources.ActionStateCompleted {
return false
}
return pulseIntelligenceActionVerifiedOutcome(record)
}
func pulseIntelligenceActionVerifiedOutcome(record unifiedresources.ActionAuditRecord) bool {
if !pulseIntelligenceActionWasApproved(record) {
return false
}
outcome := unifiedresources.NormalizeVerificationOutcome(record.VerificationOutcome)
if outcome.Status == unifiedresources.VerificationVerified {
return true
}
verification := unifiedresources.CanonicalActionVerification(record)
return verification != nil && verification.Ran && verification.Success
}
func pulseIntelligenceActionLifecycleIndicatesAttempt(event unifiedresources.ActionLifecycleEvent) bool {
switch event.State {
case unifiedresources.ActionStateExecuting, unifiedresources.ActionStateCompleted, unifiedresources.ActionStateFailed:
return true
default:
return false
}
}
func pulseIntelligenceActionWasApproved(record unifiedresources.ActionAuditRecord) bool {
if record.State == unifiedresources.ActionStateApproved {
return true
}
for _, approval := range record.Approvals {
if approval.Outcome == unifiedresources.OutcomeApproved {
return true
}
}
return false
}
func pulseIntelligenceActionWasApprovedSince(record unifiedresources.ActionAuditRecord, since time.Time) bool {
for _, approval := range record.Approvals {
if approval.Outcome != unifiedresources.OutcomeApproved {
continue
}
if approval.Timestamp.IsZero() || approval.Timestamp.Before(since) {
continue
}
return true
}
return false
}
func pulseIntelligenceActionWasRejected(record unifiedresources.ActionAuditRecord) bool {
if record.State == unifiedresources.ActionStateRejected {
return true
}
for _, approval := range record.Approvals {
if approval.Outcome == unifiedresources.OutcomeRejected {
return true
}
}
return false
}