From fa0702870a610cae5d652ef81551beb105253f07 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Wed, 1 Jul 2026 19:38:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(sealed):=20sealed=20visibility=20+=20ineff?= =?UTF-8?q?ective-suppress=20warning=20(=C2=A711=20tail=20M4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `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) --- crates/manager-core/src/apply_service.rs | 52 +++++++++++++++++------- crates/picloud-cli/src/client.rs | 6 +++ crates/picloud-cli/src/cmds/routes.rs | 2 + crates/picloud-cli/src/cmds/triggers.rs | 10 ++++- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/crates/manager-core/src/apply_service.rs b/crates/manager-core/src/apply_service.rs index e54176f..e589208 100644 --- a/crates/manager-core/src/apply_service.rs +++ b/crates/manager-core/src/apply_service.rs @@ -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 = trig_names.into_iter().map(|(n,)| n).collect(); + let inherited_handlers: HashMap = 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 = route_paths.into_iter().map(|(p,)| p).collect(); + let inherited_paths: HashMap = 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) diff --git a/crates/picloud-cli/src/client.rs b/crates/picloud-cli/src/client.rs index faf3e80..02f2832 100644 --- a/crates/picloud-cli/src/client.rs +++ b/crates/picloud-cli/src/client.rs @@ -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. diff --git a/crates/picloud-cli/src/cmds/routes.rs b/crates/picloud-cli/src/cmds/routes.rs index ab13369..2da912a 100644 --- a/crates/picloud-cli/src/cmds/routes.rs +++ b/crates/picloud-cli/src/cmds/routes.rs @@ -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); diff --git a/crates/picloud-cli/src/cmds/triggers.rs b/crates/picloud-cli/src/cmds/triggers.rs index 23f46e4..f10c8d2 100644 --- a/crates/picloud-cli/src/cmds/triggers.rs +++ b/crates/picloud-cli/src/cmds/triggers.rs @@ -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(())