Skip to content

✨ feat(notifications): notify when the maturity gate clears (#587)#597

Merged
scttbnsn merged 3 commits into
dev/v1.6from
feat/v1.6-maturity-notification
Jul 24, 2026
Merged

✨ feat(notifications): notify when the maturity gate clears (#587)#597
scttbnsn merged 3 commits into
dev/v1.6from
feat/v1.6-maturity-notification

Conversation

@scttbnsn

@scttbnsn scttbnsn commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Implements discussion #587: fire a notification the moment an update previously withheld by the maturity gate (maturityMode: mature) becomes applicable.

What was happening

While an update soaks, updateAvailable is suppressed and every notification stays silent. When the window passes, the generic update-available notification fires — but only at the next watcher cron tick (default 6-hourly), indistinguishable from a fresh detection, and the whole transition had zero test coverage.

What this adds

  • maturityGatePendingSince marker persisted while an update soaks: reset on candidate-identity change, survives container recreation via the update lifecycle cache, cleared exactly once when the gate opens.
  • Detection helper (app/maturity/gate-watch.ts) with clear-flag-first exactly-once semantics; emits for any reason the gate opens (clock elapsed, policy change, override, snooze expiry).
  • Sweep scheduler (app/maturity/scheduler.ts): store-only walk (no registry traffic) every 5 minutes by default, DD_MATURITY_SWEEP_CRON to tune, empty string disables. Same check also runs inline at every watch cycle and agent report, before report emission.
  • maturity-cleared notification kind with its own default templates. Dispatch mirrors update-available exactly: respects THRESHOLD, AUTO, include/exclude, agent scoping, and once. Symmetric dedup against update-available (both directions, recorded only on confirmed delivery) so one update is never announced twice. Action triggers are unaffected and keep applying updates on their normal scan cadence.
  • Audit trail row (dedupe-windowed) + in-app bell support + en locale labels (other locales via Crowdin).
  • Docs (triggers, update-eligibility, watchers, API) + CHANGELOG entry.

Tests

  • App: 394 files / 12,371 tests, 100% coverage on all four metrics; UI: 213 files / 4,303 tests, 100%.
  • Includes a regression for the previously-untested suppressed→matured transition (exactly one notification across the maturity-cleared event and the subsequent generic report cycle).

Also carries 🔒 security(deps): brace-expansion 5.0.7→5.0.8 (CVE-2026-14257) in app/ui/e2e lockfiles — surfaced by the osv-scanner pre-push gate on this branch.

Follow-ups deliberately not in scope

  • No dedicated SSE broadcast for the bell (it refreshes on the existing debounced events); can be added later if near-instant bell updates matter.
  • AuditView filter dropdown doesn't list container-unhealthy/agent-reconnect today either — pre-existing gap, left consistent.

Changelog

  • ✨ Added persisted maturityGatePendingSince container marker with lifecycle-aware stamping, carry-forward, and clearing
  • ✨ Added maybeEmitMaturityGateCleared exactly-once detection that clears the pending marker and emits maturity-cleared when the gate opens
  • ✨ Added cron-driven “maturity gate sweep” with configurable, validated DD_MATURITY_SWEEP_CRON (getMaturitySweepConfiguration)
  • ✨ Added maturity-cleared ordered event plumbing (emitMaturityGateCleared / registerMaturityGateCleared)
  • ✨ Added notification rule + templates + dispatch semantics for maturity-cleared, including updated delivery-success bookkeeping and once/dedup behavior against update-available
  • ✨ Added audit trail support for maturity-cleared with 5-minute deduplication keyed by container identity (fallback to container name) and incorporating pendingSince
  • ✨ Added in-app bell support, English labels, and UI/audit filtering updates for maturity-cleared
  • 🔧 Updated container/watch/agent processing to run maturity-cleared detection before container report emission (with regression coverage)
  • 🔧 Improved notification dispatch/error handling for maturity-cleared emission (isolation and test determinism tweaks)
  • 🔒 Security: Updated brace-expansion from 5.0.7 to 5.0.8 across app, UI, and E2E lockfiles (CVE-2026-14257)
  • 🐛 Added extensive regression + full-coverage tests for suppressed-to-matured transition, scheduler behavior, and audit/notification dedup edges

Concerns

  • Validate production correctness of exactly-once behavior under concurrent watch cycles and scheduler sweeps (marker clearing vs emit failures)
  • Confirm dedup key strategy (identity vs container name) and pendingSince handling matches intended business semantics
  • Ensure scheduler enablement/cron validation and overlapping-sweep protection behave as expected in all runtime configurations
  • Verify maturity-cleared notification delivery interactions with existing update-available history, retries, and batch flush semantics
  • Double-check UI bell/audit filtering paths for consistent action-id usage (maturity-cleared) and audit query behavior
  • Confirm all workspace lockfiles for brace-expansion are updated consistently and installed dependencies resolve cleanly

scttbnsn added 2 commits July 24, 2026 18:19
Fire a dedicated maturity-cleared notification the moment an update
previously withheld by maturityMode=mature becomes applicable, instead
of silently waiting for the next scan's generic update-available.

- ✨ feat(store): persist a maturityGatePendingSince marker while an
  update soaks; reset on candidate identity change, survive recreation
  via the update lifecycle cache
- ✨ feat(maturity): gate-watch detection helper with clear-flag-first
  exactly-once semantics + a store-only sweep scheduler
  (DD_MATURITY_SWEEP_CRON, default */5 min, empty disables)
- ✨ feat(triggers): maturity-cleared notification kind with its own
  templates; respects THRESHOLD/AUTO/include/exclude/once like
  update-available; symmetric dedup so an update is never announced
  twice; dedup hashes recorded only on confirmed delivery; action
  triggers unaffected
- ✨ feat(events): emitMaturityGateCleared ordered-handler event +
  audit-trail subscription (dedupe-windowed) + watch-cycle and
  AgentClient pre-report hooks
- ✨ feat(ui): bell support + en rule labels for the new kind
- 📝 docs(triggers): document the notification, sweep env var, and
  timing semantics; CHANGELOG entry
- ✅ test: full coverage incl. the previously-untested
  suppressed→matured transition regression
OOM DoS via unbounded expansion length (GHSA-mh99-v99m-4gvg); bumps the
app/ui lockfiles and the e2e override pin. Surfaced by the qlty
osv-scanner pre-push gate.
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
drydock-website Ready Ready Preview, Comment Jul 24, 2026 11:06pm
drydockdemo-website Ready Ready Preview, Comment Jul 24, 2026 11:06pm

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f155bfdf-67a1-4d42-a263-55035c21b460

📥 Commits

Reviewing files that changed from the base of the PR and between 723e579 and 76c0997.

📒 Files selected for processing (7)
  • app/event/audit-subscriptions.test.ts
  • app/event/audit-subscriptions.ts
  • app/maturity/gate-watch.test.ts
  • app/maturity/gate-watch.ts
  • app/triggers/providers/Trigger.test.ts
  • content/docs/current/api/index.mdx
  • ui/src/locales/en/listViews.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • ui/src/locales/en/listViews.json
  • content/docs/current/api/index.mdx
  • app/maturity/gate-watch.test.ts
  • app/maturity/gate-watch.ts
  • app/event/audit-subscriptions.ts
  • app/event/audit-subscriptions.test.ts

📝 Walkthrough

Walkthrough

Adds maturity-gate lifecycle tracking, policy evaluation, and a configurable cron sweep. Cleared gates emit ordered events that are audited with deduplication and routed through a new maturity-cleared notification rule. Watch and agent processing await gate-cleared detection before report emission. Startup and shutdown manage the scheduler, while UI types, bell filtering, tests, and documentation support the new event.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v1.6-maturity-notification

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/agent/AgentClient.ts (1)

767-774: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Gate detection skipped in the watcher-snapshot reconciliation branch.

handleWatcherSnapshotEvent's pending-report branch calls buildContainerReport directly (bypassing processContainer), so maybeEmitMaturityGateCleared never runs for containers reconciled that way. Detection still happens eventually via the scheduled sweep, but it's inconsistent with the other watch/report paths that now await gate detection inline.

       const pendingContainerReport = this.takePendingWatcherCycleReport(watcherName, container);
       if (pendingContainerReport) {
         this.clearPendingFreshState(container.id);
-        containerReports.push(
-          await this.buildContainerReport(container, pendingContainerReport.changed),
-        );
+        const report = await this.buildContainerReport(container, pendingContainerReport.changed);
+        await maybeEmitMaturityGateCleared(report.container);
+        containerReports.push(report);
         continue;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/agent/AgentClient.ts` around lines 767 - 774, Update the pending-report
reconciliation branch in handleWatcherSnapshotEvent to invoke
maybeEmitMaturityGateCleared for the built container report before emitting it,
matching the inline gate-detection order in processContainer. Preserve the
existing report emission and reconciliation behavior.
🧹 Nitpick comments (3)
app/triggers/providers/Trigger.test.ts (1)

4373-4377: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

DRY the optimistic dispatch microtask flush.

The triple await Promise.resolve() at Lines 4375-4379, 4447-4449, and 4573-4575 hardcodes dispatchToTriggerOptimistically’s current .then() depth; any added await/new chain links will break these tests. Extract a named helper or shared drain function for these assertion sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/triggers/providers/Trigger.test.ts` around lines 4373 - 4377, Extract the
repeated triple Promise.resolve microtask flushes in the relevant Trigger tests
into a named shared drain helper, then use that helper at all optimistic
dispatch assertion sites around dispatchToTriggerOptimistically. Keep the helper
responsible for draining the required promise chain so tests do not depend on
hardcoded .then() depth.
app/maturity/scheduler.ts (1)

62-85: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

init() isn't idempotent — re-invocation leaks the previous cron task.

If init() is called again while a cronTask is already scheduled, the old task is overwritten without calling .stop(), leaving it running forever alongside any new task (duplicate/leaked sweeps).

♻️ Proposed fix
 export function init(): void {
+  if (cronTask) {
+    cronTask.stop();
+    cronTask = undefined;
+    running = false;
+  }
   const maturitySweepConfig = getMaturitySweepConfiguration();
   const cronExpression = maturitySweepConfig.cron;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/maturity/scheduler.ts` around lines 62 - 85, Make init idempotent by
stopping any existing cronTask before scheduling a new one, using the task’s
stop method and clearing the prior reference as needed. Update the cronTask
handling in init so repeated calls cannot leave the previous maturity sweep
running alongside the replacement task.
app/maturity/gate-watch.ts (1)

8-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse hasMaturityGatePendingSince from gate-watch.ts.

scheduler.ts duplicates the predicate instead of importing it. Export it from gate-watch.ts, keeping a Container parameter, then import and use it from scheduler.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/maturity/gate-watch.ts` around lines 8 - 13, Export
hasMaturityGatePendingSince from gate-watch.ts without changing its Container
parameter or predicate behavior, then remove scheduler.ts’s duplicate
maturity-gate check and import and use this shared helper instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/event/audit-subscriptions.ts`:
- Around line 361-369: Update the dedupe key in the registerMaturityGateCleared
handler to include payload.container.pendingSince along with the container
identity, so distinct maturity-gate intervals are not suppressed within the
dedupe window. Preserve duplicate suppression for repeated emissions sharing the
same pendingSince value, and add a regression test covering two different
pendingSince values within five minutes.

In `@app/maturity/gate-watch.ts`:
- Around line 25-54: Update maybeEmitMaturityGateCleared to contain failures
from emitMaturityGateCleared: preserve clearing state, but catch handler/emit
errors and return false instead of propagating them to direct callers. Keep the
successful emission path returning true and retain the existing no-op checks.

In `@app/triggers/providers/Trigger.test.ts`:
- Around line 4559-4562: Await the asynchronous initialization in the regression
test by changing the trigger.init() call to await trigger.init(). Keep the
existing setup and later awaited init() calls unchanged, ensuring initialization
completes before the test proceeds and any rejection is observed.

In `@content/docs/current/api/index.mdx`:
- Around line 220-232: Update the documentation text at the default notification
rules description to state that eight rules are created automatically, matching
the eight entries in DEFAULT_NOTIFICATION_RULES and the payload’s total count.

In `@ui/src/locales/en/listViews.json`:
- Around line 110-113: Add the "maturity-cleared": "Maturity Cleared" entry to
the auditView.actions catalog in the English locale file, reusing the existing
maturity-cleared label and preserving the current locale structure.

---

Outside diff comments:
In `@app/agent/AgentClient.ts`:
- Around line 767-774: Update the pending-report reconciliation branch in
handleWatcherSnapshotEvent to invoke maybeEmitMaturityGateCleared for the built
container report before emitting it, matching the inline gate-detection order in
processContainer. Preserve the existing report emission and reconciliation
behavior.

---

Nitpick comments:
In `@app/maturity/gate-watch.ts`:
- Around line 8-13: Export hasMaturityGatePendingSince from gate-watch.ts
without changing its Container parameter or predicate behavior, then remove
scheduler.ts’s duplicate maturity-gate check and import and use this shared
helper instead.

In `@app/maturity/scheduler.ts`:
- Around line 62-85: Make init idempotent by stopping any existing cronTask
before scheduling a new one, using the task’s stop method and clearing the prior
reference as needed. Update the cronTask handling in init so repeated calls
cannot leave the previous maturity sweep running alongside the replacement task.

In `@app/triggers/providers/Trigger.test.ts`:
- Around line 4373-4377: Extract the repeated triple Promise.resolve microtask
flushes in the relevant Trigger tests into a named shared drain helper, then use
that helper at all optimistic dispatch assertion sites around
dispatchToTriggerOptimistically. Keep the helper responsible for draining the
required promise chain so tests do not depend on hardcoded .then() depth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e19bd10e-7fca-4d16-b151-8c87eef0b0af

📥 Commits

Reviewing files that changed from the base of the PR and between fd37530 and 723e579.

⛔ Files ignored due to path filters (4)
  • CHANGELOG.md is excluded by !CHANGELOG.md
  • app/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • e2e/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • ui/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
📒 Files selected for processing (39)
  • app/agent/AgentClient.test.ts
  • app/agent/AgentClient.ts
  • app/configuration/index.test.ts
  • app/configuration/index.ts
  • app/event/audit-subscriptions.test.ts
  • app/event/audit-subscriptions.ts
  • app/event/index.test.ts
  • app/event/index.ts
  • app/index.test.ts
  • app/index.ts
  • app/maturity/gate-watch.test.ts
  • app/maturity/gate-watch.ts
  • app/maturity/scheduler.test.ts
  • app/maturity/scheduler.ts
  • app/model/audit.d.ts
  • app/model/container.test.ts
  • app/model/container.ts
  • app/model/maturity-policy.test.ts
  • app/model/maturity-policy.ts
  • app/registry/index.test.ts
  • app/registry/index.ts
  • app/store/container.test.ts
  • app/store/container.ts
  • app/store/notification-history.ts
  • app/store/notification.ts
  • app/triggers/providers/Trigger.test.ts
  • app/triggers/providers/Trigger.ts
  • app/watchers/providers/docker/container-processing.test.ts
  • app/watchers/providers/docker/container-processing.ts
  • content/docs/current/api/index.mdx
  • content/docs/current/configuration/actions/update-eligibility.mdx
  • content/docs/current/configuration/triggers/index.mdx
  • content/docs/current/configuration/watchers/index.mdx
  • e2e/package.json
  • ui/src/locales/en/listViews.json
  • ui/src/stores/notifications.ts
  • ui/src/types/container.d.ts
  • ui/tests/components/NotificationBell.spec.ts
  • ui/tests/stores/notificationStore.spec.ts

Comment thread app/event/audit-subscriptions.ts
Comment thread app/maturity/gate-watch.ts
Comment thread app/triggers/providers/Trigger.test.ts
Comment thread content/docs/current/api/index.mdx
Comment thread ui/src/locales/en/listViews.json
- 🐛 fix(maturity): contain emit failures inside maybeEmitMaturityGateCleared
  so a throwing handler can't abort the caller's container-report cycle
- 🐛 fix(audit): include pendingSince in the maturity-cleared audit dedupe key
  so a legitimate second clearance within the window still records
- ✅ test(triggers): await trigger.init() in the maturity-cleared batch test
- 📝 docs(api): default notification rule count seven → eight
- 🔧 config(ui): add the maturity-cleared audit action label to the en catalog
@scttbnsn
scttbnsn merged commit 1125189 into dev/v1.6 Jul 24, 2026
25 checks passed
@scttbnsn
scttbnsn deleted the feat/v1.6-maturity-notification branch July 24, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant