feat(hierarchies): cross-repo template expansion (M7)
Route/trigger templates declared at a group now fan out to EVERY descendant
app in the DB subtree, not just the app nodes present in the current
`pic apply --dir`. A template change at group N reaches subtree(N) across
repos, and a removed/disabled template reaps its expansions on those
descendants too.
- group_repo: recursive `descendant_app_ids` (+ `_tx`) — inverse of the
ancestor chain CTE, app_id-ordered, depth-bounded.
- apply_service: Phase B2 expands templates into descendants of every in-tree
group not already handled in Phase B. All descendant locks are taken up
front in the single sorted batch (their union is invariant under reparenting
in-tree groups), so there is no out-of-order mid-tx locking / deadlock.
Per-recipient write caps (AppWriteRoute / AppManageTriggers /
AppSecretsRead-for-email) are required AUTHORITATIVELY IN-TX against the
post-reparent, already-locked app — checked set == written set, no TOCTOU.
expand_*_templates_tx are now self-contained (load hand-declared identities
from the tx), so collisions are caught on descendants too. Blast radius now
counts the true DB subtree. Descendant expansion errors are tagged with the
app slug (e.g. an {env} template on a NULL-environment descendant names it).
- apply_api: the pre-tx descendant authz pass is removed in favour of the
in-tx gate (sound via the hierarchy-aware effective_app_role).
- templates journey: out-of-tree descendant at depth 1 AND 2 receives the
expansion and is reaped on template removal.
- doc §4.5: strike the in-apply-only scope limitation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -402,9 +402,13 @@ Two distinct constraints:
|
||||
> expansion is validated with the same rules its hand-declared counterpart gets — routes:
|
||||
> reserved-path rejection + structural path/host parse + host-claim; triggers: the per-kind shape
|
||||
> check (cron schedule, queue timeout, …) — so a `{var:…}`-injected reserved path, unclaimed strict
|
||||
> host, or malformed schedule/timeout can't slip in. **Scope:** expansion targets app NODES present in the tree apply
|
||||
> (the common `pic apply --dir` case), not yet every descendant in another repo — deleting a template
|
||||
> leaves expansions in out-of-apply descendants until they're re-applied (the apply warns).
|
||||
> host, or malformed schedule/timeout can't slip in. **Scope:** expansion targets **every descendant app
|
||||
> in the DB subtree** (M7, 2026-06-28), not only the app nodes present in the apply — a template change at
|
||||
> group N fans out to `subtree(N)` across repos, and removing a template reaps its expansions on those
|
||||
> descendants too (the apply locks each extra descendant in `app_id` order). The actor must hold the
|
||||
> per-recipient write cap (`AppWriteRoute`/`AppManageTriggers`/`AppSecretsRead`) on each descendant; the
|
||||
> hierarchy-aware `effective_app_role` makes a group admin/editor implicitly authorized on its subtree, so
|
||||
> this is sound rather than an escalation, and a narrowly-scoped principal is refused (naming the app).
|
||||
> `{var:NAME}` resolves **in-transaction** (M6, 2026-06-28) — a var set in the *same* apply is visible to
|
||||
> the template immediately, so var + template converge in one apply. A
|
||||
> trigger template whose only change is a non-identity field (e.g. dispatch_mode) is a no-op on
|
||||
|
||||
Reference in New Issue
Block a user