docs(suppress): flag advisory-by-default trust model; test dangling-suppress warning (§11 tail review)

Two review findings.

1. Per-app opt-out changes the group-template guarantee from "runs on every
   descendant" to "runs unless the descendant declines" — a footgun for
   compliance hooks (an audit/security trigger a tenant can silently opt out
   of). There is no non-suppressible flag. Document this in design §4.5 +
   CLAUDE.md, and record the deferred mitigation: a `sealed`/`mandatory`
   group-template marker the trigger anti-join + route filter skip (cheap —
   both filters are centralized).

2. The dangling-suppress warning path had no coverage. The suppress journey
   now applies a `[suppress] triggers=["does-not-exist"]`, asserts the apply
   still succeeds and the report warns "no effect".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-01 18:59:20 +02:00
parent b74e1a401b
commit 3df37b2b85
3 changed files with 39 additions and 3 deletions

View File

@@ -161,6 +161,32 @@ fn app_suppresses_inherited_route_template() {
"re-apply should not create a second suppression:\n{report}"
);
// A suppress reference matching NO inherited template is a soft warning
// (typo guard), not an error — the apply still succeeds.
let dangling = format!(
"[app]\nslug = \"{blog}\"\nname = \"Blog\"\n\n\
[suppress]\nroutes = [\"/ghello\"]\ntriggers = [\"does-not-exist\"]\n"
);
fs::write(&apath, &dangling).unwrap();
let out = common::pic_as(&env)
.args(["apply", "--file"])
.arg(&apath)
.output()
.expect("apply dangling");
assert!(
out.status.success(),
"a dangling suppress must not fail apply"
);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
combined.contains("does-not-exist") && combined.to_lowercase().contains("no effect"),
"a dangling suppress must warn that it matches no inherited template:\n{combined}"
);
// Drop the [suppress] block + prune → blog re-inherits the route.
let bare = format!("[app]\nslug = \"{blog}\"\nname = \"Blog\"\n");
fs::write(&apath, &bare).unwrap();