fix(suppress): don't over-decline a nearer descendant's own route/trigger
A group's template suppression is "coarse by reference" (path for routes, handler-name for triggers), and both resolution points dropped ANY inherited row matching that reference across the whole subtree — including one a NEARER descendant group deliberately re-declared. So an ancestor group G that declines a far-ancestor's `/x` (or `audit` handler) would also silently kill a child group H's OWN `/x` / `audit`-bound trigger at the same reference, violating the documented "an owner can only decline what it inherits, never a descendant's own rows" invariant. Fix: gate each decline on the chain DEPTH of the suppressor vs the target's owner — a suppressor at depth `d_s` may only decline a row whose owner is strictly ABOVE it (`target_depth > d_s`): - Routes: `list_route_suppressions` now returns `(app, path, suppressor_depth)`; the rebuild folds it to the min depth per `(app, path)` and `compile_effective_routes` skips an inherited route only when `route.depth > suppressor_depth`. (This also subsumes the old `depth > 0` inherited-only gate.) - Triggers: the dispatch anti-join gains `AND sc.depth < c.depth` (the suppressor's chain depth below the trigger owner's), correlating on the outer `chain c` that every kv/docs/files/pubsub match query already binds. An app's own suppression is depth 0 → still declines anything it inherits; a group's suppression declines only what that group itself inherits. `sealed` still overrides. No schema change. Pinned by a new `compile_effective_routes` unit test (a depth-1 descendant route survives a depth-2 suppression that declines a depth-3 template) and a new `group_suppression` DB test (same for triggers); all existing suppression / sealed / template journeys stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,12 @@ use crate::trigger_config::BackoffShape;
|
||||
/// 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.
|
||||
///
|
||||
/// `sc.depth < c.depth` is the over-decline guard: a suppressor at depth
|
||||
/// `sc.depth` may only decline a trigger whose owner is strictly ABOVE it
|
||||
/// (`c.depth` is the trigger owner's depth in the outer query's `chain c`), so
|
||||
/// a suppression of a far-ancestor template never clobbers a NEARER descendant
|
||||
/// group's OWN trigger that happens to bind a same-named handler.
|
||||
pub(crate) const TRIGGER_SUPPRESSION_ANTIJOIN: &str = " \
|
||||
AND NOT EXISTS ( \
|
||||
SELECT 1 FROM template_suppressions ts \
|
||||
@@ -34,7 +40,8 @@ pub(crate) const TRIGGER_SUPPRESSION_ANTIJOIN: &str = " \
|
||||
WHERE ts.target_kind = 'trigger' \
|
||||
AND LOWER(ts.reference) = LOWER(s.name) \
|
||||
AND t.group_id IS NOT NULL \
|
||||
AND t.sealed = FALSE)";
|
||||
AND t.sealed = FALSE \
|
||||
AND sc.depth < c.depth)";
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TriggerRepoError {
|
||||
|
||||
Reference in New Issue
Block a user