package observation import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "sort" "time" "github.com/tegwick/fluid-core/internal/contract" "github.com/tegwick/fluid-core/internal/evidence" ) // PressureRegistry is the durable inventory of material interface pressure. // // ArchitectureBlueprint.md section 9 requires deduplication, aggregation, // cohort segmentation, frequency tracking, severity, confidence, linked // hypotheses and disposition — and says plainly that pressure may remain // unresolved on purpose. Not every observed mismatch deserves adaptation. type PressureRegistry struct { store evidence.Store iface contract.InterfaceID now func() time.Time } // NewPressureRegistry returns a registry backed by the evidence store. func NewPressureRegistry(store evidence.Store, iface contract.InterfaceID) *PressureRegistry { return &PressureRegistry{store: store, iface: iface, now: time.Now} } // ErrDismissed reports an attempt to reopen a deliberately closed pressure. var ErrDismissed = errors.New("pressure was dismissed and will not be reopened automatically") // pressureID derives a stable identifier from a finding's fingerprint. // // Deriving rather than allocating is what makes ingest idempotent: re-running // analysis over the same window updates one record instead of minting a new one // on every pass. func pressureID(iface contract.InterfaceID, fingerprint string) contract.PressureID { sum := sha256.Sum256([]byte(string(iface) + "\x00" + fingerprint)) return contract.PressureID("P-" + hex.EncodeToString(sum[:6])) } // Record folds a finding into the registry, creating or updating one pressure. // // A finding that matches an existing record extends its window and refreshes // its counts rather than replacing it: first_seen is evidence about how long // the interface has had this problem, and overwriting it would erase the age // that makes a pressure worth prioritizing. func (r *PressureRegistry) Record(ctx context.Context, f Finding) (contract.FluidPressure, error) { id := pressureID(r.iface, f.Fingerprint) existing, err := r.Get(ctx, id) switch { case err == nil: if existing.Status == contract.FluidPressureStatusDISMISSED { // An operator decided this is not worth acting on. Silently // resurrecting it would make the dismissal meaningless. return existing, fmt.Errorf("%w: %s", ErrDismissed, id) } return r.update(ctx, existing, f) case errors.Is(err, evidence.ErrNotFound): return r.create(ctx, id, f) default: return contract.FluidPressure{}, err } } func (r *PressureRegistry) create(ctx context.Context, id contract.PressureID, f Finding) (contract.FluidPressure, error) { observations := int64(f.Occurrences) consumers := int64(f.Consumers) severity := contract.UnitInterval(f.Severity) confidence := contract.UnitInterval(f.Confidence) p := contract.FluidPressure{ SchemaVersion: "0.1", ID: id, InterfaceID: r.iface, Class: f.Class, FirstSeen: f.FirstSeen, LastSeen: f.LastSeen, AffectedCohorts: f.Cohorts, Frequency: &contract.FluidPressureFrequency{ Observations: &observations, IndependentConsumers: &consumers, }, Severity: &severity, Confidence: &confidence, Summary: f.Summary, EvidenceRefs: f.Evidence, Status: contract.FluidPressureStatusOPEN, } if err := r.put(ctx, p); err != nil { return contract.FluidPressure{}, err } if err := r.event(ctx, p, "PRESSURE_OPENED", f.Summary); err != nil { return contract.FluidPressure{}, err } return p, nil } func (r *PressureRegistry) update(ctx context.Context, p contract.FluidPressure, f Finding) (contract.FluidPressure, error) { if f.FirstSeen.Before(p.FirstSeen) { p.FirstSeen = f.FirstSeen } if f.LastSeen.After(p.LastSeen) { p.LastSeen = f.LastSeen } observations := int64(f.Occurrences) consumers := int64(f.Consumers) p.Frequency = &contract.FluidPressureFrequency{ Observations: &observations, IndependentConsumers: &consumers, } severity := contract.UnitInterval(f.Severity) confidence := contract.UnitInterval(f.Confidence) p.Severity = &severity p.Confidence = &confidence p.Summary = f.Summary p.AffectedCohorts = mergeCohorts(p.AffectedCohorts, f.Cohorts) p.EvidenceRefs = mergeRefs(p.EvidenceRefs, f.Evidence) // Fresh evidence for something previously explained means it is back. if p.Status == contract.FluidPressureStatusADDRESSED { p.Status = contract.FluidPressureStatusOPEN if err := r.put(ctx, p); err != nil { return contract.FluidPressure{}, err } return p, r.event(ctx, p, "PRESSURE_REOPENED", "new evidence observed after the pressure was marked addressed") } if err := r.put(ctx, p); err != nil { return contract.FluidPressure{}, err } return p, r.event(ctx, p, "PRESSURE_OBSERVED", f.Summary) } // RecordAll folds a batch of findings into the registry. // // Dismissed pressures are skipped rather than treated as errors: hitting one // is the expected outcome of re-analysing a window an operator has already // triaged. func (r *PressureRegistry) RecordAll(ctx context.Context, findings []Finding) ([]contract.FluidPressure, error) { var out []contract.FluidPressure for _, f := range findings { p, err := r.Record(ctx, f) if errors.Is(err, ErrDismissed) { continue } if err != nil { return out, err } out = append(out, p) } return out, nil } // Get returns one pressure record. func (r *PressureRegistry) Get(ctx context.Context, id contract.PressureID) (contract.FluidPressure, error) { body, err := r.store.Record(ctx, contract.KindPressure, string(id)) if err != nil { return contract.FluidPressure{}, err } var doc contract.PressureDocument if err := json.Unmarshal(body, &doc); err != nil { return contract.FluidPressure{}, fmt.Errorf("decode pressure %s: %w", id, err) } return doc.FluidPressure, nil } // List returns pressures, optionally filtered by status. func (r *PressureRegistry) List(ctx context.Context, status contract.FluidPressureStatus) ([]contract.FluidPressure, error) { records, err := r.store.Records(ctx, contract.KindPressure) if err != nil { return nil, err } out := make([]contract.FluidPressure, 0, len(records)) for _, body := range records { var doc contract.PressureDocument if err := json.Unmarshal(body, &doc); err != nil { continue } if status != "" && doc.FluidPressure.Status != status { continue } out = append(out, doc.FluidPressure) } // Most severe first: the registry is a work queue as much as an inventory. sort.Slice(out, func(i, j int) bool { si, sj := unit(out[i].Severity), unit(out[j].Severity) if si != sj { return si > sj } return out[i].ID < out[j].ID }) return out, nil } // SetStatus moves a pressure through its lifecycle. // // Transitions are recorded as events rather than only as a field, so a // dismissal can be traced to whoever made it and why. func (r *PressureRegistry) SetStatus(ctx context.Context, id contract.PressureID, status contract.FluidPressureStatus, actor contract.Actor, reason string) error { if !status.Valid() { return fmt.Errorf("unknown pressure status %q", status) } if reason == "" { // A status change with no reason is not auditable, and dismissals // without a reason are how a registry quietly loses its evidence. return errors.New("a status change requires a reason") } p, err := r.Get(ctx, id) if err != nil { return err } previous := p.Status p.Status = status if err := r.put(ctx, p); err != nil { return err } return r.eventBy(ctx, p, "PRESSURE_STATUS_CHANGED", actor, fmt.Sprintf("%s -> %s: %s", previous, status, reason)) } // LinkHypothesis records that a hypothesis addresses this pressure. func (r *PressureRegistry) LinkHypothesis(ctx context.Context, id contract.PressureID, h contract.HypothesisID) error { if err := contract.RequireKind(string(h), contract.KindHypothesis); err != nil { return err } p, err := r.Get(ctx, id) if err != nil { return err } for _, existing := range p.LinkedHypotheses { if existing == h { return nil } } p.LinkedHypotheses = append(p.LinkedHypotheses, h) if p.Status == contract.FluidPressureStatusOPEN { p.Status = contract.FluidPressureStatusANALYZING } if err := r.put(ctx, p); err != nil { return err } return r.event(ctx, p, "PRESSURE_HYPOTHESIS_LINKED", fmt.Sprintf("linked %s", h)) } func (r *PressureRegistry) put(ctx context.Context, p contract.FluidPressure) error { body, err := json.Marshal(contract.PressureDocument{FluidPressure: p}) if err != nil { return err } return r.store.PutRecord(ctx, contract.KindPressure, string(p.ID), body) } func (r *PressureRegistry) event(ctx context.Context, p contract.FluidPressure, kind, reason string) error { return r.eventBy(ctx, p, kind, contract.Actor{Type: contract.ActorTypeSystem, ID: "fluid-pressure-engine"}, reason) } func (r *PressureRegistry) eventBy(ctx context.Context, p contract.FluidPressure, kind string, actor contract.Actor, reason string) error { return r.store.AppendEvent(ctx, contract.FluidEvent{ SchemaVersion: "0.1", ID: contract.EventID(fmt.Sprintf("EV-%s-%d", p.ID, r.now().UnixNano())), OccurredAt: r.now().UTC(), EntityType: contract.FluidEventEntityTypePressure, EntityID: string(p.ID), EventType: kind, Actor: actor, Reason: reason, EvidenceRefs: p.EvidenceRefs, }) } func unit(v *contract.UnitInterval) float64 { if v == nil { return 0 } return float64(*v) } func mergeCohorts(a, b []contract.CohortID) []contract.CohortID { seen := map[contract.CohortID]struct{}{} var out []contract.CohortID for _, list := range [][]contract.CohortID{a, b} { for _, c := range list { if _, ok := seen[c]; ok { continue } seen[c] = struct{}{} out = append(out, c) } } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out } func mergeRefs(a, b []contract.EvidenceRef) []contract.EvidenceRef { seen := map[contract.EvidenceRef]struct{}{} var out []contract.EvidenceRef for _, list := range [][]contract.EvidenceRef{a, b} { for _, s := range list { if _, ok := seen[s]; ok { continue } seen[s] = struct{}{} out = append(out, s) } } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out }