detect

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package detect contains Chronos's pattern detectors and the engine that fans observations out across them.

Each detector implements Detector and emits zero or more domain.Signals tagged with its PatternType. Detectors are pure functions of their input plus a configuration snapshot — they do no I/O. The Engine orchestrates a slice of detectors so adding a new pattern type is a one-file change.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClassifyConfidence added in v0.5.0

func ClassifyConfidence(n, minPoints int, cfg *config.Config) domain.ConfidenceClass

ClassifyConfidence buckets a signal into tentative / established / strong based on how many supporting observations the detector saw relative to its MIN_POINTS floor. The thresholds are multipliers on MIN_POINTS so the classifier scales naturally across detectors — a trend with MinPoints=4 and n=20 is "strong" by the same rule as a cross-scope-correlation with MinPoints=5 and n=25.

Returns the empty class (zero ConfidenceClass) when n or minPoints is non-positive; the caller decides whether to omit the field or leave it empty in the signal. A non-positive minPoints would be a detector misconfiguration; failing silent here avoids surfacing noise downstream.

Types

type Anomaly

type Anomaly struct {
	// contains filtered or unexported fields
}

Anomaly detects PatternTypeAnomaly: an entity whose most recent state is unlike the most recent states of its peers in the same scope. It is the cross-entity dual of Recurrence — Recurrence asks "have peers been here before?", Anomaly asks "are peers nowhere near where this entity is right now?"

Method: cosine similarity between the subject's current state and every other entity's current state. If the *highest* similarity across peers is below CHRONOS_ANOMALY_MAX_SIM, the subject is isolated → emit. Strength is 1 - max similarity (more isolated = stronger anomaly); Confidence scales by peer-count saturation.

Evidence is a "peer_distance" record per peer, sorted by similarity descending so the closest peer (still below threshold) appears first.

func NewAnomaly

func NewAnomaly(cfg *config.Config) *Anomaly

NewAnomaly wires an Anomaly detector from configuration.

func (*Anomaly) Detect

func (a *Anomaly) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect compares each entity's most recent state against the most recent states of other entities in the same scope.

func (*Anomaly) Pattern

func (a *Anomaly) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

type ChangePoint added in v0.4.0

type ChangePoint struct {
	// contains filtered or unexported fields
}

ChangePoint detects PatternTypeChangePoint: a step change in the mean of the outcome metric. Distinct from Spike/Drop (short-lived deviations) and Trend (continuous slope) — a change point is a sustained shift between two regimes.

Method: best-split mean-shift test. For each candidate split index k in [minSide, n-minSide), compute the standardised mean shift

shift(k) = |mean(left) - mean(right)| / pooled_stddev

where pooled_stddev is sqrt((var_left*(n_l-1) + var_right*(n_r-1)) / (n_l+n_r-2)). Emit a signal at the index that maximises shift, gated by CHRONOS_CHANGEPOINT_MIN_SHIFT. Strength is the shift scaled to [0, 1] by CHRONOS_CHANGEPOINT_MIN_SHIFT (so a shift of 2× the threshold gets strength ≈ 0.5 above the floor); Confidence scales by sample-size factor.

Evidence is one record per regime ("regime_before", "regime_after") carrying mean, stddev, and n.

func NewChangePoint added in v0.4.0

func NewChangePoint(cfg *config.Config) *ChangePoint

NewChangePoint wires a ChangePoint detector from configuration.

func (*ChangePoint) Detect added in v0.4.0

func (c *ChangePoint) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect emits at most one change-point signal per series — the best split that exceeds the configured shift threshold.

func (*ChangePoint) Pattern added in v0.4.0

func (c *ChangePoint) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

type Correlation

type Correlation struct {
	// contains filtered or unexported fields
}

Correlation detects PatternTypeCorrelation: two series in the same scope whose outcome metrics move together (positive r) or opposite (negative r). Pearson correlation is computed on the last min(len(a), len(b)) outcomes of each series — the alignment is by ordinal index, so this assumes both series share roughly the same observation cadence. Adapters that ingest at very different cadences will produce noisy correlations.

One signal is emitted per pair, with Series being the lexicographically smaller of the two entity IDs (so signals are deterministic across runs and not duplicated). Evidence is a "pair_correlation" record pointing at the partner series.

Cost is O(N²) in series count per scope; the engine's MaxSignalsPerRun cap and SignalRepository filters provide downstream triage.

func NewCorrelation

func NewCorrelation(cfg *config.Config) *Correlation

NewCorrelation wires a Correlation detector from configuration.

func (*Correlation) Detect

func (c *Correlation) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect runs pairwise Pearson correlations within the scope.

func (*Correlation) Pattern

func (c *Correlation) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

type CrossScopeCorrelation added in v0.4.0

type CrossScopeCorrelation struct {
	// contains filtered or unexported fields
}

CrossScopeCorrelation detects PatternTypeCrossScopeCorrelation: two series in DIFFERENT scopes whose outcome metrics move together (positive r) or opposite (negative r). The within-scope Correlation detector misses these by design — it groups by scope. This detector runs once across the full state list as a CrossScopeDetector.

Method: align observations by ordinal index (mirroring the within-scope detector's assumption that adapters with similar cadence are comparable). Pearson correlation gated on CHRONOS_CROSS_SCOPE_MIN and CHRONOS_CROSS_SCOPE_MIN_POINTS.

Cost is O(N²) in series count *globally*, so the threshold is stricter than the within-scope default (0.8 vs 0.7) to keep noise down. Operators with many scopes should tighten further.

Each emitted signal is owned by the lex-smaller (scope, series) pair, with the partner pair carried in evidence. The signal's ScopeID is the lex-smaller scope.

func NewCrossScopeCorrelation added in v0.4.0

func NewCrossScopeCorrelation(cfg *config.Config) *CrossScopeCorrelation

NewCrossScopeCorrelation wires the detector from configuration.

func (*CrossScopeCorrelation) CrossDetect added in v0.4.0

func (c *CrossScopeCorrelation) CrossDetect(_ context.Context, states []chronos.EntityState) []domain.Signal

CrossDetect computes pairwise correlations across every (scope, series) pair and emits one signal per pair above threshold.

func (*CrossScopeCorrelation) Pattern added in v0.4.0

Pattern reports the PatternType this detector emits.

type CrossScopeDetector added in v0.4.0

type CrossScopeDetector interface {
	// Pattern returns the PatternType this detector emits.
	Pattern() domain.PatternType

	// CrossDetect runs once over every state in the input. ScopeID on
	// emitted signals is uuid.Nil unless the detector picks one of the
	// participating scopes; choose deterministically so signals are
	// stable across runs.
	CrossDetect(ctx context.Context, states []chronos.EntityState) []domain.Signal
}

CrossScopeDetector is a detector whose input is the full state list across every scope, not a single per-scope group. The engine calls CrossDetect exactly once per Detect run, after the per-scope detectors have finished.

func DefaultCrossScopeDetectors added in v0.4.0

func DefaultCrossScopeDetectors(cfg *config.Config) []CrossScopeDetector

DefaultCrossScopeDetectors returns the standard cross-scope detector set. Today this is just CrossScopeCorrelation; future detectors that need to compare across scopes (e.g. cross-scope outlier clusters) will be added here.

type Detector

type Detector interface {
	// Pattern returns the PatternType this detector emits.
	Pattern() domain.PatternType

	// Detect produces signals for the given scope. The states slice is
	// guaranteed to be sorted by Timestamp ascending and to share the
	// supplied scopeID. May return an empty slice.
	Detect(ctx context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal
}

Detector is the contract every pattern detector implements. Each detector is responsible for one PatternType.

Detect operates on the states for a single scope. The engine groups the input by scope before calling Detect. Detectors must not mutate the input slice. Returned signals must already be Validated by the detector — the engine asserts but does not fix invalid output.

func DefaultDetectors

func DefaultDetectors(cfg *config.Config) []Detector

DefaultDetectors returns the standard detector set wired with cfg. The full vision vocabulary is implemented: Recurrence, Trend, Spike, Drop, Stall, Anomaly, Seasonality, Correlation. Callers wanting a custom subset construct an Engine directly with explicit detectors.

type Drop

type Drop struct {
	// contains filtered or unexported fields
}

Drop detects PatternTypeDrop: a sharp negative deviation of the most recent outcome from the rolling baseline. See Spike for method.

func NewDrop

func NewDrop(cfg *config.Config) *Drop

NewDrop wires a Drop detector from configuration.

func (*Drop) Detect

func (d *Drop) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect scans each series for negative-direction deviations.

func (*Drop) Pattern

func (d *Drop) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine fans observations out to a set of detectors, deduplicates and sorts the resulting signals, and applies a global cap.

Construction is explicit: callers register the detectors they want. The default set (see DefaultDetectors) includes Recurrence; new pattern detectors are added by appending to the slice.

func NewEngine

func NewEngine(cfg *config.Config, detectors ...Detector) *Engine

NewEngine builds an Engine. If detectors is empty, DefaultDetectors is used. Cross-scope detectors come from DefaultCrossScopeDetectors when not set explicitly via WithCrossScopeDetectors.

func (*Engine) Detect

func (e *Engine) Detect(ctx context.Context, states []chronos.EntityState) []domain.Signal

Detect groups states by scope and runs every detector against each group. The combined output is sorted by detected-at descending, confidence descending, and capped at cfg.MaxSignalsPerRun.

func (*Engine) WithCrossScopeDetectors added in v0.4.0

func (e *Engine) WithCrossScopeDetectors(ds []CrossScopeDetector) *Engine

WithCrossScopeDetectors replaces the engine's cross-scope detector set. Pass an empty slice to disable cross-scope detection entirely.

func (*Engine) WithParallelDetectors added in v0.4.0

func (e *Engine) WithParallelDetectors(on bool) *Engine

WithParallelDetectors enables parallel execution of per-scope detectors. Each (scope, detector) pair runs in its own goroutine. Detectors are pure functions of their input plus configuration so the parallelism is safe.

Off by default — sequential execution keeps deterministic signal ordering for tests and small deployments. Operators with many detectors and many scopes flip this on.

type OutlierCluster added in v0.4.0

type OutlierCluster struct {
	// contains filtered or unexported fields
}

OutlierCluster detects PatternTypeOutlierCluster: a temporal window in which several series in the same scope go anomalous together. Distinct from per-series Anomaly (cross-entity): this is the cohort-level signal that something systemic is happening.

Method: for each series, compute z-scores against the series's own rolling baseline (last [SpikeWindow] points; reuses the Spike detector's window so configuration stays consistent). A per-(series, time) "outlier event" is emitted when |z| ≥ CHRONOS_OUTLIER_CLUSTER_Z. Outlier events are then grouped into time buckets of width CHRONOS_OUTLIER_CLUSTER_WINDOW; any bucket containing distinct-series count ≥ CHRONOS_OUTLIER_CLUSTER_MIN_SERIES becomes a signal. The signal's Series is uuid.Nil (cohort-level, not entity-level); evidence carries one row per participating series with its peak |z|.

func NewOutlierCluster added in v0.4.0

func NewOutlierCluster(cfg *config.Config) *OutlierCluster

NewOutlierCluster wires an OutlierCluster detector from configuration.

func (*OutlierCluster) Detect added in v0.4.0

func (o *OutlierCluster) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect groups outlier events into time windows and emits one signal per qualifying cluster.

func (*OutlierCluster) Pattern added in v0.4.0

func (o *OutlierCluster) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

type Recurrence

type Recurrence struct {
	// contains filtered or unexported fields
}

Recurrence detects PatternTypeRecurrence: situations where a subject entity is in a state that other entities in the same scope have been in before. Evidence comes from cross-entity cosine similarity over historical states.

The detector is intentionally simple — it emits the *signal* that "we have seen this before"; what it means (good, bad, neutral) is Nous's concern, not Chronos's.

func NewRecurrence

func NewRecurrence(cfg *config.Config) *Recurrence

NewRecurrence wires a Recurrence detector from configuration.

func (*Recurrence) Detect

func (r *Recurrence) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect compares each entity's most recent state against historical states of *other* entities in the same scope. When at least MinSampleSize peers exceed SimilarityThreshold, a signal is emitted.

func (*Recurrence) Pattern

func (r *Recurrence) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

type Seasonality

type Seasonality struct {
	// contains filtered or unexported fields
}

Seasonality detects PatternTypeSeasonality: a periodic structure in the outcome series. Method: autocorrelation at lags [SeasonalityMinPeriod, n/2]; the lag with the highest autocorrelation is the candidate period. If that peak exceeds CHRONOS_SEASONALITY_MIN_AUTOCORR the detector emits.

Strength is the autocorrelation value; Confidence scales by the sample-size factor against 2*SeasonalityMinPoints.

Evidence is a single "autocorrelation_peak" record carrying the detected period (lag) and the autocorrelation value.

func NewSeasonality

func NewSeasonality(cfg *config.Config) *Seasonality

NewSeasonality wires a Seasonality detector from configuration.

func (*Seasonality) Detect

func (s *Seasonality) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect emits one seasonality signal per series whose strongest autocorrelation peak passes the threshold.

func (*Seasonality) Pattern

func (s *Seasonality) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

type Spike

type Spike struct {
	// contains filtered or unexported fields
}

Spike detects PatternTypeSpike: a sharp positive deviation of the most recent outcome relative to a rolling baseline of the previous CHRONOS_SPIKE_WINDOW points. Drop is the symmetric negative case (see Drop).

Strength is the z-score normalised against a saturation point of 5 (z=5 → strength 1.0). Confidence equals strength: for spike-style detectors the magnitude of the deviation *is* the confidence.

Evidence is a single "baseline_deviation" record carrying z, the baseline mean and stddev, and the observed value.

func NewSpike

func NewSpike(cfg *config.Config) *Spike

NewSpike wires a Spike detector from configuration.

func (*Spike) Detect

func (s *Spike) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect scans each series and emits at most one spike per series (the most recent point, if it crosses the threshold).

func (*Spike) Pattern

func (s *Spike) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

type Stall

type Stall struct {
	// contains filtered or unexported fields
}

Stall detects PatternTypeStall: the outcome metric shows little to no relative variation over the analysis window.

Method: outcomes are normalised by their first value (or, when the first value is zero, by the mean) so the stddev is scale-invariant. A signal is emitted when the normalised stddev is below CHRONOS_STALL_MAX_STDDEV and the series has at least CHRONOS_STALL_MIN_POINTS observations. Strength reflects flatness (1.0 = perfectly flat, 0.0 = at-threshold); Confidence scales flatness by a sample-size factor.

Evidence is a single "variance_window" record carrying the normalised stddev, mean, and n.

func NewStall

func NewStall(cfg *config.Config) *Stall

NewStall wires a Stall detector from configuration.

func (*Stall) Detect

func (s *Stall) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect emits one stall signal per series whose normalised stddev passes below the threshold.

func (*Stall) Pattern

func (s *Stall) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

type Trend

type Trend struct {
	// contains filtered or unexported fields
}

Trend detects PatternTypeTrend: a sustained directional movement of the outcome metric over the analysis window.

Method: ordinary-least-squares linear regression of outcome against ordinal index (0..n-1). The detector emits when |slope| exceeds CHRONOS_TREND_MIN_SLOPE *and* the regression's R² is meaningful. Strength is R² (how cleanly the data is a line); Confidence is R² scaled by a sample-size factor.

Evidence: a single "regression_summary" record carrying slope, R², intercept, and n.

func NewTrend

func NewTrend(cfg *config.Config) *Trend

NewTrend wires a Trend detector from configuration.

func (*Trend) Detect

func (t *Trend) Detect(_ context.Context, scopeID uuid.UUID, states []chronos.EntityState) []domain.Signal

Detect emits one trend signal per series whose outcome regression passes the slope and fit thresholds.

func (*Trend) Pattern

func (t *Trend) Pattern() domain.PatternType

Pattern reports the PatternType this detector emits.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL