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:
@@ -167,8 +167,27 @@ impl RouteRepository for PostgresRouteRepository {
|
||||
}
|
||||
|
||||
async fn list_route_suppressions(&self) -> Result<Vec<(AppId, String)>, ScriptRepositoryError> {
|
||||
// §11 tail M1: a route suppression is owned by an app (declines for
|
||||
// itself) OR a group (declines for its whole subtree). Expand each
|
||||
// group-owned suppression across every descendant app via the all-apps
|
||||
// `app_chain` CTE (the same one `list_effective` uses): the result is
|
||||
// `(effective_app_id, path)` pairs the rebuild consumes unchanged — a
|
||||
// group suppression appears once per descendant app.
|
||||
let rows: Vec<(Uuid, String)> = sqlx::query_as(
|
||||
"SELECT app_id, reference FROM template_suppressions WHERE target_kind = 'route'",
|
||||
"WITH RECURSIVE app_chain AS ( \
|
||||
SELECT a.id AS effective_app_id, a.id AS owner_app, \
|
||||
NULL::uuid AS owner_group, a.group_id AS next_group, 0 AS depth \
|
||||
FROM apps a \
|
||||
UNION ALL \
|
||||
SELECT ac.effective_app_id, NULL::uuid, g.id, g.parent_id, ac.depth + 1 \
|
||||
FROM groups g JOIN app_chain ac ON g.id = ac.next_group \
|
||||
WHERE ac.depth < 64 \
|
||||
) \
|
||||
SELECT DISTINCT ac.effective_app_id, ts.reference \
|
||||
FROM app_chain ac \
|
||||
JOIN template_suppressions ts \
|
||||
ON (ts.app_id = ac.owner_app OR ts.group_id = ac.owner_group) \
|
||||
WHERE ts.target_kind = 'route'",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
@@ -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)";
|
||||
|
||||
Reference in New Issue
Block a user