feat(suppress): suppression journey + docs (§11 tail S5)

- Journey tests/suppress.rs: a group declares a `[[routes]]` template; a
  descendant app applies `[suppress] routes=["/ghello"]` → stops serving it
  (via `routes match`), a sibling still serves it, `pic suppress ls --app`
  shows it, re-apply is a NoOp; pruning the block re-inherits. The
  trigger-side filter + isolation are pinned at the repo layer by
  template_suppression.rs.
- Docs: design §4.5 (per-app opt-out closing the deferred gap for both
  templates — coarse-by-reference, inheritance-only, the two consumption
  points; group-level suppress still deferred), CLAUDE.md current-focus.

Full journey suite 121/121; workspace tests 34 suites/0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-01 08:05:06 +02:00
parent 9601046e29
commit b74e1a401b
4 changed files with 200 additions and 4 deletions

View File

@@ -417,9 +417,27 @@ Two distinct constraints:
> Authoring is `[[routes]]` on a `[group]`; read-only `pic routes ls --group`. **Disabled semantic
> (§4.3):** the shadow check runs *after* the `enabled` filter, so a **disabled** own-route does not
> claim its binding — an enabled ancestor template at the same path then falls through and serves
> ("disabled = absent"). The corollary: a descendant can't 404 an inherited route by disabling a
> same-path own route. **Deferred:** per-app route opt-out (the true way to decline an inherited
> route), and multi-node route-snapshot propagation (cluster mode).
> ("disabled = absent"). To actually 404 an inherited route, an app SUPPRESSES its path (below).
> **Deferred:** multi-node route-snapshot propagation (cluster mode).
>
> **Shipped — per-app opt-out of inherited templates (suppression).** A descendant could shadow a
> template but not decline one; for triggers there was no decline path at all (an app's own identical
> trigger double-fires). An app now declares `[suppress]` — `triggers = [...]` (handler SCRIPT names)
> and `routes = [...]` (PATHS) — to opt out of inherited group templates. **Coarse by reference,** not
> a full definition (template row-ids churn on re-apply; a reference is stable, so re-apply is a NoOp
> and a reference may decline several templates bound to the same script/path). Marker table
> `0058_template_suppressions.sql` — app-only (`app_id NOT NULL`, ON DELETE CASCADE; a group just
> wouldn't declare the template), a `target_kind` discriminator (`trigger`|`route`), reconciled with the
> extension-point marker pattern (idempotent create, prunable delete → the template re-inherits).
> **Consumed at the two existing resolution points:** the trigger dispatch queries gain a correlated
> `NOT EXISTS` anti-join (excluding a group-owned trigger whose handler the app suppresses, gated to
> `t.group_id IS NOT NULL`), and `compile_effective_routes` drops an inherited (`depth > 0`) route at a
> suppressed path (loaded once per rebuild via `RouteRepository::list_route_suppressions`). **Suppression
> is inheritance-only** — the `group_id IS NOT NULL` / `depth > 0` gates mean an app can only decline
> what it inherits, never its own resource nor a sibling's (pinned by `tests/template_suppression.rs`).
> A dangling suppress (matching no inherited template) is an apply-time **warning**, not an error;
> read-only `pic suppress ls --app`. **Deferred:** group-level suppression (decline an ancestor's
> template for a whole subtree).
### 4.6 Secrets & `pull`