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:
MechaCat02
2026-07-01 19:38:18 +02:00
parent 247e2836b8
commit fa0702870a
4 changed files with 53 additions and 17 deletions

View File

@@ -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)

View File

@@ -1405,6 +1405,9 @@ pub struct TriggerTemplateDto {
#[serde(default)]
pub script: String,
pub enabled: bool,
/// §11 tail: `true` for a sealed (non-suppressible) template.
#[serde(default)]
pub sealed: bool,
}
/// One row of the §11 tail route-template report.
@@ -1423,6 +1426,9 @@ pub struct RouteTemplateDto {
#[serde(default)]
pub dispatch: String,
pub enabled: bool,
/// §11 tail: `true` for a sealed (non-suppressible) template.
#[serde(default)]
pub sealed: bool,
}
/// One row of the §11.6 shared-collection report.

View File

@@ -50,6 +50,7 @@ pub async fn ls_group(group: &str, mode: OutputMode) -> Result<()> {
"script",
"dispatch",
"enabled",
"sealed",
]);
for r in rows {
table.row([
@@ -60,6 +61,7 @@ pub async fn ls_group(group: &str, mode: OutputMode) -> Result<()> {
r.script,
r.dispatch,
r.enabled.to_string(),
r.sealed.to_string(),
]);
}
table.print(mode);

View File

@@ -47,9 +47,15 @@ pub async fn ls_group(group: &str, mode: OutputMode) -> Result<()> {
let creds = config::resolve()?;
let client = Client::from_creds(&creds)?;
let rows = client.group_triggers_list(group).await?;
let mut table = Table::new(["kind", "target", "script", "enabled"]);
let mut table = Table::new(["kind", "target", "script", "enabled", "sealed"]);
for t in rows {
table.row([t.kind, t.target, t.script, t.enabled.to_string()]);
table.row([
t.kind,
t.target,
t.script,
t.enabled.to_string(),
t.sealed.to_string(),
]);
}
table.print(mode);
Ok(())