feat(sealed): sealed visibility + ineffective-suppress warning (§11 tail M4)
- `pic triggers ls --group` / `routes ls --group` gain a `sealed` column
(threaded through TriggerTemplateInfo/RouteTemplateInfo + the two DTOs).
- dangling_suppress_warnings now also warns when a suppress reference matches
only SEALED inherited templates ("... is sealed — the suppression has no
effect") via `bool_or(NOT sealed)` per handler/path — making the mandatory
guarantee observable at plan/apply time, alongside the existing typo guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -530,6 +530,8 @@ pub struct TriggerTemplateInfo {
|
||||
pub target: String,
|
||||
pub script: String,
|
||||
pub enabled: bool,
|
||||
/// §11 tail: `true` for a sealed (non-suppressible) template.
|
||||
pub sealed: bool,
|
||||
}
|
||||
|
||||
/// One row of the read-only §11 tail route-template report (`pic routes ls
|
||||
@@ -545,6 +547,8 @@ pub struct RouteTemplateInfo {
|
||||
pub script: String,
|
||||
pub dispatch: String,
|
||||
pub enabled: bool,
|
||||
/// §11 tail: `true` for a sealed (non-suppressible) template.
|
||||
pub sealed: bool,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -2081,6 +2085,7 @@ impl ApplyService {
|
||||
target,
|
||||
script: name_by_id.get(&t.script_id).cloned().unwrap_or_default(),
|
||||
enabled: t.enabled,
|
||||
sealed: t.sealed,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
@@ -2122,6 +2127,7 @@ impl ApplyService {
|
||||
script: name_by_id.get(&r.script_id).cloned().unwrap_or_default(),
|
||||
dispatch: r.dispatch_mode.as_str().to_string(),
|
||||
enabled: r.enabled,
|
||||
sealed: r.sealed,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -2153,47 +2159,63 @@ impl ApplyService {
|
||||
if bundle.suppress_triggers.is_empty() && bundle.suppress_routes.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
// Inherited (ancestor-group) trigger handler names + route paths.
|
||||
let trig_names: Vec<(String,)> = sqlx::query_as(&format!(
|
||||
// Inherited (ancestor-group) trigger handler names + route paths, each
|
||||
// tagged with whether ANY matching template is un-sealed (i.e. actually
|
||||
// suppressible). `bool_or(NOT sealed)` = true iff at least one match can
|
||||
// be declined; false = every match is `sealed` (§11 tail), so the
|
||||
// suppression is inert; absent = no inherited template at all (a typo).
|
||||
let trig_names: Vec<(String, bool)> = sqlx::query_as(&format!(
|
||||
"{CHAIN_LEVELS_CTE} \
|
||||
SELECT DISTINCT LOWER(s.name) FROM triggers t \
|
||||
SELECT LOWER(s.name), bool_or(NOT t.sealed) FROM triggers t \
|
||||
JOIN scripts s ON s.id = t.script_id \
|
||||
JOIN chain c ON t.group_id = c.group_owner \
|
||||
WHERE t.group_id IS NOT NULL",
|
||||
WHERE t.group_id IS NOT NULL \
|
||||
GROUP BY LOWER(s.name)",
|
||||
))
|
||||
.bind(app_id.into_inner())
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ApplyError::Backend(e.to_string()))?;
|
||||
let inherited_handlers: HashSet<String> = trig_names.into_iter().map(|(n,)| n).collect();
|
||||
let inherited_handlers: HashMap<String, bool> = trig_names.into_iter().collect();
|
||||
|
||||
let route_paths: Vec<(String,)> = sqlx::query_as(&format!(
|
||||
let route_paths: Vec<(String, bool)> = sqlx::query_as(&format!(
|
||||
"{CHAIN_LEVELS_CTE} \
|
||||
SELECT DISTINCT r.path FROM routes r \
|
||||
SELECT r.path, bool_or(NOT r.sealed) FROM routes r \
|
||||
JOIN chain c ON r.group_id = c.group_owner \
|
||||
WHERE r.group_id IS NOT NULL",
|
||||
WHERE r.group_id IS NOT NULL \
|
||||
GROUP BY r.path",
|
||||
))
|
||||
.bind(app_id.into_inner())
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ApplyError::Backend(e.to_string()))?;
|
||||
let inherited_paths: HashSet<String> = route_paths.into_iter().map(|(p,)| p).collect();
|
||||
let inherited_paths: HashMap<String, bool> = route_paths.into_iter().collect();
|
||||
|
||||
let mut out = Vec::new();
|
||||
for r in &bundle.suppress_triggers {
|
||||
if !inherited_handlers.contains(&r.to_lowercase()) {
|
||||
out.push(format!(
|
||||
match inherited_handlers.get(&r.to_lowercase()) {
|
||||
None => out.push(format!(
|
||||
"suppress: no inherited trigger bound to script `{r}` — the \
|
||||
suppression has no effect"
|
||||
));
|
||||
)),
|
||||
Some(false) => out.push(format!(
|
||||
"suppress: the inherited trigger bound to script `{r}` is \
|
||||
sealed — the suppression has no effect"
|
||||
)),
|
||||
Some(true) => {}
|
||||
}
|
||||
}
|
||||
for r in &bundle.suppress_routes {
|
||||
if !inherited_paths.contains(r) {
|
||||
out.push(format!(
|
||||
match inherited_paths.get(r) {
|
||||
None => out.push(format!(
|
||||
"suppress: no inherited route at path `{r}` — the \
|
||||
suppression has no effect"
|
||||
));
|
||||
)),
|
||||
Some(false) => out.push(format!(
|
||||
"suppress: the inherited route at path `{r}` is sealed — the \
|
||||
suppression has no effect"
|
||||
)),
|
||||
Some(true) => {}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
||||
Reference in New Issue
Block a user