// Package alerts is the proactive alert engine, the watch loop. A best in world capture intelligence does // not wait to be asked: it WATCHES the account and tells the analyst, unprompted, when something actionable // happens. This module adds the MARKET EVENT arm to the existing scheduled bench watch // (staffing.Service.RecomputeProactive) plus a typed, persisted, UI surfaceable alert. // // Four triggers fire per sweep, all RLS scoped to the caller's org: // // - clearance_expiring: a candidate the owner holds whose clearance lapses within the window, a staffing // risk to flag before the reinvestigation lapses. // - recompete_winnable: a recently posted federal opportunity in an agency the owner already shows interest // in (a requirement for that agency, or a recorded bid) whose period of performance ends inside the // forward recompete window. This is the market event arm: GLOBAL PUBLIC opportunity_records joined to the // owner's own interest, RLS scoping only the interest read. // - gap_fillable: an open requirement with no green evaluation that the bench NOW structurally covers (at // least one candidate holds at least the required clearance level AND at least four fifths of the required // skills, the SAME structural test as ai.BenchGapForRequirement, integer arithmetic, no float). This is a // STRUCTURAL coverage test, never a match score: the frozen engine stays the only scorer. // - collision: a candidate (most sharply a scarce cleared one) proposed or approved on the slates of two or // more DISTINCT competing OPEN opportunities at once, the double commit of a scarce cleared person across // competing bids. It is an ADVISORY finding with a deterministic best fit suggested primary a human // resolves; it never auto rejects a slate member. // // Each alert UPSERTs ON CONFLICT (organization_id, dedupe_key), so a second sweep of the same event changes // nothing new (idempotent). The sweep writes ONE counts only audit event (aedt.alert.swept) with the per type // counts and NO PII: no candidate id, no agency value, just counts. package alerts // The four alert types, mirroring the alerts.alert_type CHECK constraint. Each names a distinct actionable // event the watch loop detects. Collision is the cross opportunity deconfliction finding: a candidate proposed // or approved on two or more distinct competing open opportunities at once. const ( TypeRecompeteWinnable = "recompete_winnable" TypeClearanceExpiring = "clearance_expiring" TypeGapFillable = "gap_fillable" TypeCollision = "collision" ) // The three severities, mirroring the alerts.severity CHECK constraint, ordered from quiet to loud. const ( SeverityInfo = "info" SeverityWatch = "watch" SeverityUrgent = "urgent" ) // The three statuses the UI moves an alert through, mirroring the alerts.status CHECK constraint. const ( StatusUnread = "unread" StatusRead = "read" StatusDismissed = "dismissed" ) // The three subject kinds an alert points at, the owner's own record. An empty subject is allowed (the // column defaults to the empty string) but the three triggers always set one. const ( SubjectOpportunity = "opportunity" SubjectCandidate = "candidate" SubjectRequirement = "requirement" ) // validTypes, validSeverities, and validStatuses are the closed sets the DB CHECK constraints enforce. The // service and handler validate against them before a write so a bad value is a clean rejection rather than a // raw constraint violation. var ( validTypes = map[string]bool{ TypeRecompeteWinnable: true, TypeClearanceExpiring: true, TypeGapFillable: true, TypeCollision: true, } validSeverities = map[string]bool{ SeverityInfo: true, SeverityWatch: true, SeverityUrgent: true, } validStatuses = map[string]bool{ StatusUnread: true, StatusRead: true, StatusDismissed: true, } ) // IsValidType reports whether t is one of the four allowed alert types. func IsValidType(t string) bool { return validTypes[t] } // IsValidSeverity reports whether s is one of the three allowed severities. func IsValidSeverity(s string) bool { return validSeverities[s] } // IsValidStatus reports whether s is one of the three allowed statuses. func IsValidStatus(s string) bool { return validStatuses[s] } // Alert is one persisted alert row returned to the UI. subject_id is omitted when the alert carries no // subject; created_at and updated_at are RFC3339. type Alert struct { ID string `json:"id"` AlertType string `json:"alert_type"` Severity string `json:"severity"` SubjectType string `json:"subject_type,omitempty"` SubjectID string `json:"subject_id,omitempty"` Title string `json:"title"` Detail string `json:"detail,omitempty"` Status string `json:"status"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // SweepSummary is the per type count of alerts the sweep upserted, the only thing the sweep returns to the // caller and the only thing the counts only audit event records. It carries NO PII: no candidate id, no // agency value, no requirement title, just counts. type SweepSummary struct { ClearanceExpiring int `json:"clearance_expiring"` RecompeteWinnable int `json:"recompete_winnable"` GapFillable int `json:"gap_fillable"` Collision int `json:"collision"` } // Total is the sum of the per type counts, the total number of alerts the sweep upserted. func (s SweepSummary) Total() int { return s.ClearanceExpiring + s.RecompeteWinnable + s.GapFillable + s.Collision } // dedupeKey builds the stable per (org, the thing alerted) dedupe key the UPSERT keys on. The org scoping is // the UNIQUE (organization_id, dedupe_key) constraint, so the key itself carries only the type prefix and the // subject id; a re sweep of the same event yields the same key and the UPSERT refreshes the one row. Keeping // this pure and named lets the unit test pin the construction. func dedupeKey(alertType, subjectID string) string { return alertType + ":" + subjectID }