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:
@@ -767,20 +767,11 @@ impl ApplyService {
|
||||
));
|
||||
}
|
||||
}
|
||||
// §11 tail: only an APP opts out of inherited templates. A group that
|
||||
// doesn't want a template simply doesn't declare it (the CLI already
|
||||
// rejects `[suppress]` on a `[group]`; this guards a hand-rolled wire
|
||||
// Bundle — the reconcile's `owner.app_id().expect(...)` would otherwise
|
||||
// panic on a group).
|
||||
if matches!(owner, ApplyOwner::Group(_))
|
||||
&& (!bundle.suppress_triggers.is_empty() || !bundle.suppress_routes.is_empty())
|
||||
{
|
||||
return Err(ApplyError::Invalid(
|
||||
"a group cannot declare suppressions — only an app opts out of \
|
||||
inherited templates"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
// §11 tail M1: both an app and a group may declare suppressions — an
|
||||
// app declines an inherited template for itself, a group for its whole
|
||||
// subtree. Both are inheritance-only (the dispatch filters gate to
|
||||
// group-owned templates on the suppressing owner's chain), so no
|
||||
// owner-kind guard here.
|
||||
self.validate_bundle(bundle, inherited_endpoints)
|
||||
}
|
||||
|
||||
@@ -1065,17 +1056,22 @@ impl ApplyService {
|
||||
}
|
||||
|
||||
// 3e. Per-app suppression markers (§11 tail) — insert each Create
|
||||
// (idempotent). App-only; deletes happen in prune → the template
|
||||
// re-inherits. Key is `"{target_kind}:{reference}"`; split on the FIRST
|
||||
// `:` so a route reference containing `:` (a param path `/users/:id`)
|
||||
// survives.
|
||||
// (idempotent). Owner-polymorphic (§11 tail M1): an app declines for
|
||||
// itself, a group for its whole subtree. Deletes happen in prune → the
|
||||
// template re-inherits. Key is `"{target_kind}:{reference}"`; split on
|
||||
// the FIRST `:` so a route reference containing `:` (a param path
|
||||
// `/users/:id`) survives.
|
||||
for ch in &plan.suppressions {
|
||||
if ch.op == Op::Create {
|
||||
let app_id = owner.app_id().expect("suppressions are app-only");
|
||||
let (kind, reference) = ch.key.split_once(':').expect("suppression key has ':'");
|
||||
crate::suppression_repo::insert_suppression_tx(&mut *tx, app_id, kind, reference)
|
||||
.await
|
||||
.map_err(|e| ApplyError::Backend(e.to_string()))?;
|
||||
crate::suppression_repo::insert_suppression_tx(
|
||||
&mut *tx,
|
||||
owner.as_script_owner(),
|
||||
kind,
|
||||
reference,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ApplyError::Backend(e.to_string()))?;
|
||||
report.suppressions_created += 1;
|
||||
}
|
||||
}
|
||||
@@ -1188,15 +1184,17 @@ impl ApplyService {
|
||||
}
|
||||
}
|
||||
|
||||
// Per-app suppression markers are prunable config too (§11 tail).
|
||||
// Suppression markers are prunable config too (§11 tail).
|
||||
// Removing a marker re-inherits the template.
|
||||
for ch in &plan.suppressions {
|
||||
if ch.op == Op::Delete {
|
||||
let app_id = owner.app_id().expect("suppressions are app-only");
|
||||
let (kind, reference) =
|
||||
ch.key.split_once(':').expect("suppression key has ':'");
|
||||
crate::suppression_repo::delete_suppression_tx(
|
||||
&mut *tx, app_id, kind, reference,
|
||||
&mut *tx,
|
||||
owner.as_script_owner(),
|
||||
kind,
|
||||
reference,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ApplyError::Backend(e.to_string()))?;
|
||||
@@ -2221,17 +2219,14 @@ impl ApplyService {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Read-only §11 tail report: an app's own suppression markers (the
|
||||
/// inherited templates it declines). Group owner → empty (groups don't
|
||||
/// suppress). Backs `pic suppress ls --app`.
|
||||
/// Read-only §11 tail report: an owner's own suppression markers (the
|
||||
/// inherited templates it declines). An app declines for itself, a group
|
||||
/// for its subtree. Backs `pic suppress ls --app` / `--group`.
|
||||
pub async fn suppression_report(
|
||||
&self,
|
||||
owner: ApplyOwner,
|
||||
) -> Result<Vec<SuppressionInfo>, ApplyError> {
|
||||
let ApplyOwner::App(app_id) = owner else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let rows = crate::suppression_repo::list_for_app(&self.pool, app_id)
|
||||
let rows = crate::suppression_repo::list_for_owner(&self.pool, owner.as_script_owner())
|
||||
.await
|
||||
.map_err(|e| ApplyError::Backend(e.to_string()))?;
|
||||
Ok(rows
|
||||
@@ -2529,14 +2524,13 @@ impl ApplyService {
|
||||
crate::group_collection_repo::list_all_for_owner(&self.pool, owner.as_script_owner())
|
||||
.await
|
||||
.map_err(|e| ApplyError::Backend(e.to_string()))?;
|
||||
// §11 tail per-app suppression markers — app-only (a group never
|
||||
// suppresses; it just wouldn't declare the template).
|
||||
let suppressions = match owner {
|
||||
ApplyOwner::App(app_id) => crate::suppression_repo::list_for_app(&self.pool, app_id)
|
||||
// §11 tail suppression markers declared directly at this node
|
||||
// (§11 tail M1: owner-polymorphic — an app declines for itself, a group
|
||||
// for its subtree).
|
||||
let suppressions =
|
||||
crate::suppression_repo::list_for_owner(&self.pool, owner.as_script_owner())
|
||||
.await
|
||||
.map_err(|e| ApplyError::Backend(e.to_string()))?,
|
||||
ApplyOwner::Group(_) => Vec::new(),
|
||||
};
|
||||
.map_err(|e| ApplyError::Backend(e.to_string()))?;
|
||||
Ok(CurrentState {
|
||||
scripts,
|
||||
routes,
|
||||
|
||||
Reference in New Issue
Block a user