feat(suppress): owner-polymorphic suppression repo + reconcile (M1.2)

suppression_repo takes a ScriptOwner (app or group), binding the matching
owner column. apply_service load_current / create / prune / suppression_report
all thread owner.as_script_owner(); the validate_bundle_for group-suppress
rejection and the manifest parse guard are dropped — a [group] may now declare
[suppress] to decline a template it inherits from a higher ancestor. No
runtime consumption effect yet (the chain filters land in M1.3/M1.4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-01 20:04:32 +02:00
parent cdef97a634
commit 86c57e2e43
3 changed files with 110 additions and 86 deletions

View File

@@ -73,16 +73,12 @@ impl Manifest {
}
// A group node owns scripts + vars (+ secret names) and, as of §11 tail,
// ROUTE TEMPLATES + EVENT trigger TEMPLATES (kv/docs/files/pubsub) that
// fan out live to descendant apps. The stateful trigger kinds
// fan out live to descendant apps. As of §11 tail M1 a group may also
// declare `[suppress]` to decline a template it inherits from a higher
// ancestor, for its whole subtree. The stateful trigger kinds
// (cron/queue/email) stay app-only — they need per-app state/secrets →
// materialization.
if m.group.is_some() {
if !m.suppress.is_empty() {
anyhow::bail!(
"a [group] cannot declare [suppress] — only an app opts out of \
inherited templates; a group simply doesn't declare the template"
);
}
let t = &m.triggers;
if !t.cron.is_empty() || !t.queue.is_empty() || !t.email.is_empty() {
anyhow::bail!(
@@ -808,7 +804,7 @@ mod tests {
}
#[test]
fn app_suppress_parses_group_suppress_rejected() {
fn app_and_group_suppress_parse() {
// §11 tail: an [app] declares [suppress] to opt out of inherited
// templates — script names (triggers) + paths (routes).
let m = Manifest::parse(
@@ -819,13 +815,14 @@ mod tests {
assert_eq!(m.suppress.triggers, ["audit"]);
assert_eq!(m.suppress.routes, ["/hello"]);
// A [group] cannot suppress — it simply wouldn't declare the template.
let err = Manifest::parse(
// §11 tail M1: a [group] MAY suppress — it declines a template it
// inherits from a higher ancestor, for its whole subtree.
let g = Manifest::parse(
"[group]\nslug = \"acme\"\nname = \"ACME\"\n\n\
[suppress]\ntriggers = [\"audit\"]\n",
)
.expect_err("group [suppress] is rejected");
assert!(err.to_string().contains("suppress"), "got: {err}");
.expect("group [suppress] must parse");
assert_eq!(g.suppress.triggers, ["audit"]);
// An unknown key inside [suppress] is a hard error (deny_unknown_fields).
let typo = "[app]\nslug = \"x\"\nname = \"X\"\n\n[suppress]\ntrigger = [\"a\"]\n";