feat(suppress): consume group suppressions via the chain (M1.3+M1.4)

Both suppression filters now match an owner's own OR any ancestor group's
suppression on the firing app's chain:
- trigger anti-join joins the `chain` CTE (ts.app_id = sc.app_owner OR
  ts.group_id = sc.group_owner) instead of ts.app_id = $1;
- list_route_suppressions expands group-owned suppressions across descendants
  via the all-apps app_chain CTE, yielding (effective_app_id, path) the rebuild
  consumes unchanged.

A child group declining a parent template opts out its whole subtree; a sibling
subtree still inherits; sealed still overrides. Pinned by
tests/group_suppression.rs; per-app suppression + sealed regressions green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-01 20:07:06 +02:00
parent 86c57e2e43
commit f60fa36b0b
3 changed files with 292 additions and 7 deletions

View File

@@ -17,16 +17,21 @@ use uuid::Uuid;
use crate::config_resolver::CHAIN_LEVELS_CTE;
use crate::trigger_config::BackoffShape;
/// §11 tail per-app opt-out: exclude an INHERITED (group-owned) trigger whose
/// handler script name the firing app suppresses. Appended to each dispatch
/// match query's WHERE clause; `$1` is the firing app_id (already bound by the
/// `CHAIN_LEVELS_CTE`). The `t.group_id IS NOT NULL` guard keeps an app's OWN
/// trigger unsuppressable (an app can only decline what it inherits).
/// §11 tail opt-out: exclude an INHERITED (group-owned) trigger whose handler
/// script name a suppression on the firing app's chain declines. Appended to
/// each dispatch match query's WHERE clause; `$1` is the firing app_id (already
/// bound by the `CHAIN_LEVELS_CTE`). §11 tail M1: the anti-join joins the
/// `chain` CTE, so a suppression owned by the firing app OR any ANCESTOR GROUP
/// on its chain applies — a group declines a template for its whole subtree.
/// The `t.group_id IS NOT NULL` guard keeps an app's OWN trigger unsuppressable
/// (an owner can only decline what it inherits); `t.sealed = FALSE` keeps a
/// mandatory template non-declinable.
pub(crate) const TRIGGER_SUPPRESSION_ANTIJOIN: &str = " \
AND NOT EXISTS ( \
SELECT 1 FROM template_suppressions ts \
JOIN scripts s ON s.id = t.script_id \
WHERE ts.app_id = $1 AND ts.target_kind = 'trigger' \
JOIN chain sc ON (ts.app_id = sc.app_owner OR ts.group_id = sc.group_owner) \
WHERE ts.target_kind = 'trigger' \
AND LOWER(ts.reference) = LOWER(s.name) \
AND t.group_id IS NOT NULL \
AND t.sealed = FALSE)";