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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user