feat(routes): author group route templates in manifest + reconcile (§11 tail R4)

Open the declarative path so a [group] can declare route TEMPLATES.

- manifest.rs: drop the blanket "[group] cannot declare [[routes]]"
  rejection (routes join the event trigger templates a group may own).
- apply_service:
  - validate_bundle_for: remove the group routes rejection. A group
    route's `script` is validated as binding a group-owned ENDPOINT via
    `resolve_inherited_targets_for(Group)`, now extended to scan
    bundle.routes as well as bundle.triggers — so a template binding the
    group's own (declared or pre-existing) endpoint resolves, and one
    pointing at a module or a missing name is a 422.
  - insert_bundle_route takes a ScriptOwner (was app-only): a group node
    writes a group_id route TEMPLATE, an app node an app-owned route.
  - load_current(Group) loads the group's own routes via list_for_group
    so re-apply is a NoOp and the template is prune-able.

Host-claim validation stays App-only (already guarded) — a template has
no single app; a descendant serves it on whatever host it claims (so
templates use host_kind = any in practice).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-30 21:54:55 +02:00
parent 96277e1e7c
commit 8f1ba39a8a
2 changed files with 29 additions and 32 deletions

View File

@@ -68,15 +68,11 @@ impl Manifest {
}
}
// A group node owns scripts + vars (+ secret names) and, as of §11 tail,
// EVENT trigger TEMPLATES (kv/docs/files/pubsub) that fan out live to
// descendant apps. Routes stay app-only; so do the stateful trigger
// kinds (cron/queue/email need per-app state/secrets → materialization).
// ROUTE TEMPLATES + EVENT trigger TEMPLATES (kv/docs/files/pubsub) that
// fan out live to descendant apps. The stateful trigger kinds
// (cron/queue/email) stay app-only — they need per-app state/secrets →
// materialization.
if m.group.is_some() {
if !m.routes.is_empty() {
anyhow::bail!(
"a [group] manifest cannot declare [[routes]] — routes bind to an app"
);
}
let t = &m.triggers;
if !t.cron.is_empty() || !t.queue.is_empty() || !t.email.is_empty() {
anyhow::bail!(