Skip to content

feat(notifications): Slice 2 — transaction-log rule-regression projector#685

Merged
remyluslosius merged 2 commits into
mainfrom
feat/notifications-slice2-regression-projector
Jun 25, 2026
Merged

feat(notifications): Slice 2 — transaction-log rule-regression projector#685
remyluslosius merged 2 commits into
mainfrom
feat/notifications-slice2-regression-projector

Conversation

@remyluslosius

Copy link
Copy Markdown
Contributor

Summary

Notifications Slice 2 (per the change-driven design §3/§9): the bell's headline use case — a rule that was passing is now failing. Slice 1 wired the durable per-user feed + the alert-engine channel (host unreachable/recovered, drift). This adds the second producer: a transaction-log regression projector.

When a scan flips passing rules to failing, the operator gets one grouped per-host notificationweb-01: 3 rules regressed (1 critical) — severity-ranked and deep-linked to the host, instead of nothing (the alert engine doesn't classify rule-level changes).

What it does

  • internal/notifyfeed/projector.goProjector.ProjectScan(scanID, hostID) reads the scan's transactions for the host and records one grouped rule_regression notification per active recipient:
    • Regression = state_changed → fail (a passing/skipped/errored rule now fails).
    • New critical finding = first_seen → fail only when severity=critical and the host has prior scan history. On a host's first scan every rule is first_seen (baseline) — counting those would flood the bell with "N rules regressed", so they're suppressed.
    • Grouping: group_key = rule_regression:<host> — a burst of N rules in one scan is one row, and a later scan's regressions collapse onto (and re-surface unread) the same row rather than piling up. Severity = highest among regressed rules; title summarizes counts.
  • internal/worker/scan_worker.goRegressionProjector interface + nil-safe Config/field; called best-effort after Writer.Apply succeeds. A feed-write failure logs and is swallowed — it never fails or retries the scan (the compliance state already persisted).
  • Wiring in both scan-worker boot paths: cmd/openwatch/main.go (serve, reusing the Slice-1 feed store) and cmd/openwatch/worker.go (dedicated worker).

Spec & tests

  • system-notifications v1.4.0: new C-07 + AC-12/13/14 (grouping/severity-rank, first-scan suppression, no-op + collapse). specter check green (114 specs); specter coverage --strictness annotation = system-notifications 14/14 ACs, 100%.
  • DB-backed tests in internal/notifyfeed/projector_test.go (grouping, severity ranking, first-scan suppression, recovery/severity-churn no-op, two-scan collapse + re-surface).

Scope notes

  • No frontend change — the new rows render in the existing Slice-1 bell drawer (generic title/body/severity/link).
  • Recipient scoping stays Slice-1 (all active users): host:read is granted to every role today, so "users who can see the host" is everyone. Finer per-host scoping awaits a host-ACL model (design §7) and is deferred.
  • Band drops (design §3) come from the scheduler/posture producer, not the transaction log — out of this projector's scope.

🤖 Generated with Claude Code

The bell's second producer: when a scan flips a passing rule to failing,
surface ONE grouped per-host in-app notification ("web-01: 3 rules regressed
(1 critical)") instead of the alert-only Slice 1 feed.

- internal/notifyfeed/projector.go — Projector.ProjectScan reads a scan's
  transactions for the host and records one grouped "rule_regression"
  notification per active recipient. A regression is state_changed->fail; a
  first_seen->fail counts only when severity=critical AND the host has prior
  scan history (a host's first scan is all-first_seen baseline, not a
  regression — must not flood the bell). group_key is per host so a burst
  collapses to one row and later scans re-surface it; severity is the highest
  among regressed rules; title summarizes counts.
- internal/worker/scan_worker.go — RegressionProjector interface + nil-safe
  Config/field; called best-effort after Apply succeeds (a feed write never
  fails or retries the scan).
- wired in both scan-worker boot paths: cmd/openwatch/main.go (serve, reusing
  the Slice-1 feed store) and cmd/openwatch/worker.go (dedicated worker).
- spec system-notifications v1.4.0: C-07 + AC-12/13/14 (grouping/severity,
  first-scan suppression, no-op + collapse). DB-backed tests in
  internal/notifyfeed/projector_test.go.

No frontend change: the new rows render in the existing Slice-1 bell drawer.
…sion guard

The leading bullet used 'compliance regressions' (a domain term), which trips
TestChangelog_RegressionsNameAVersion (regression-FIX entries must name the
version that regressed). Reword to avoid the trigger word; this is a feature,
not a code-regression fix, so no version token is warranted.
@remyluslosius remyluslosius merged commit 76f8939 into main Jun 25, 2026
13 checks passed
@remyluslosius remyluslosius deleted the feat/notifications-slice2-regression-projector branch June 25, 2026 16:12
remyluslosius added a commit that referenced this pull request Jun 25, 2026
Bump version.env to 0.2.0-rc.16 and roll the CHANGELOG [Unreleased]
accumulator into a dated section. Bundles the notifications action-queue
work since rc.15: rule-regression projector (#685), RBAC-scoped governance
queue (#686), and exception-expiry lifecycle (#687).

Local: changelog + version-consistency + package-build + fips tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant