docs(stateful-templates): group email materialization is shipped (M5.5)

Update the design doc §4.5 and CLAUDE.md current-focus: email joins
cron+queue as a materialized stateful template via the shared-group-secret
model; drop the email deferral and the stale "email rejected on a group"
note; retarget "Next" to multi-node / multi-repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-02 20:31:23 +02:00
parent da83222ea1
commit 186b6f1dcc
2 changed files with 24 additions and 16 deletions

View File

@@ -392,21 +392,29 @@ Two distinct constraints:
> sibling-subtree app never sees the template (pinned by `tests/group_trigger_templates.rs`). Authoring
> is `[[triggers.kv]]` (etc.) on a `[group]`; read-only `pic triggers ls --group`.
>
> **Stateful templates via MATERIALIZATION (✅, M5 — cron + queue).** The stateful kinds can't resolve
> live (each needs a per-app row: cron `last_fired_at`, the queue one-consumer advisory lock), so a group
> `[[triggers.cron]]` / `[[triggers.queue]]` template is **materialized** into an app-owned copy per
> descendant — `materialized_from = template.id` (`0062`) — by `materialize::rematerialize_stateful_templates`.
> That reconciler (all-apps `app_chain` CTE ⋈ group-owned stateful templates, a precise create/delete diff
> that preserves `last_fired_at`) runs full-live at the route-rebuild chokepoints: apply (single + tree),
> app create/delete, group reparent. The dispatch paths are **unchanged** — the scheduler + queue consumer
> gained `AND t.app_id IS NOT NULL`, so a group TEMPLATE is never dispatched directly; only its per-app
> copies are. A queue copy is **skipped-with-warning** when the app already fills that queue's consumer
> slot (the one-consumer invariant). Pinned by `tests/stateful_templates.rs` + the `stateful_templates`
> journey. **Deferred:** **email** templates (per-descendant inbound-secret resolution + resealing needs
> the master key threaded through the materialization hooks + a nullable-ciphertext / secret-ref schema —
> a bounded follow-up; a `[group]` email template is cleanly rejected at apply for now); a per-app
> `materialized` column in `pic triggers ls --app`; cross-owner dedup (a descendant re-declaring an
> identical trigger double-fires — "overlapping triggers coexist").
> **Stateful templates via MATERIALIZATION (✅, M5 — cron + queue + email).** The stateful kinds can't
> resolve live (each needs a per-app row: cron `last_fired_at`, the queue one-consumer advisory lock, the
> email sealed inbound secret), so a group `[[triggers.cron|queue|email]]` template is **materialized**
> into an app-owned copy per descendant — `materialized_from = template.id` (`0062`) — by
> `materialize::rematerialize_stateful_templates`. That reconciler (all-apps `app_chain` CTE ⋈ group-owned
> stateful templates, a precise create/delete diff that preserves `last_fired_at`) runs full-live at the
> route-rebuild chokepoints: apply (single + tree), app create/delete, group reparent. The dispatch paths
> are **unchanged** — the scheduler + queue consumer + `email_inbound_target` gained `AND t.app_id IS NOT
> NULL`, so a group TEMPLATE is never dispatched directly (nor invocable via its own webhook URL); only its
> per-app copies are. A queue copy is **skipped-with-warning** when the app already fills that queue's
> consumer slot (the one-consumer invariant).
>
> **Email uses the shared-group-secret model (M5.5).** Rather than a per-descendant reseal (the original
> deferral, which would have needed the master key threaded into every CRUD hook), the email template
> resolves its `inbound_secret_ref` against the **group's own** secret store **once at apply** and seals it
> onto the template row. Email secrets seal v0/no-AAD (`seal_legacy`), so the ciphertext is portable across
> rows — materialization copies the sealed bytes **verbatim** into each descendant (no master key, no
> reseal, no new schema column). All descendant email webhooks validate against the one group HMAC secret;
> each copy has its own `trigger_id` / inbound URL. A group email template whose group secret is unset
> fails apply hard (same as the app path). Pinned by `tests/stateful_templates.rs` + the
> `stateful_templates` journey. **Deferred:** a per-app `materialized` column in `pic triggers ls --app`;
> cross-owner dedup (a descendant re-declaring an identical trigger double-fires — "overlapping triggers
> coexist"); the per-app (non-shared) email secret model.
>
> **Shipped — group ROUTE templates (live, inherited).** The same live model, adapted to routes. A
> `[group]` declares a `[[routes]]` template binding a group-owned endpoint; `routes` gained a