feat(routes): owner-polymorphic Route + list_effective expansion query (§11 tail R2)

Make the route layer owner-aware ahead of the live rebuild. `Route.app_id`
becomes Option + a `group_id` (mirrors `Trigger`); `NewRoute` carries a
`ScriptOwner` so the interactive API passes App and the reconcile can pass
a group. `insert_route_tx` writes both owner columns; `RouteRow` selects
`group_id` everywhere (the interactive delete/list 404 a group template,
which is apply-managed, not app-addressable).

Adds the two repo methods the live rebuild + apply need:
- `list_for_group` — a group's own route templates (apply diff, ls).
- `list_effective` — the all-apps generalization of CHAIN_LEVELS_CTE: for
  every app, walk its ancestor chain and join routes owned at each level,
  tagging each with the effective (firing) app + the owner's chain depth.
  Runs only on a RouteTable rebuild, never per request; validated with
  EXPLAIN against the live schema.

`compile_route` now takes the effective app_id explicitly (reused next
commit for the per-app expansion); `compile_routes` skips group templates
(no single app) — they materialize via the effective path. Runtime
behavior is unchanged this commit: the table still rebuilds from
`list_all`, and no group routes can exist yet (authoring lands in R4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-30 21:39:07 +02:00
parent 0a583aa467
commit 469e8526d7
5 changed files with 180 additions and 40 deletions

View File

@@ -7,7 +7,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{AppId, ScriptId};
use crate::{AppId, GroupId, ScriptId};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
@@ -72,10 +72,14 @@ impl DispatchMode {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Route {
pub id: Uuid,
/// Owning app. Always equals `scripts.app_id` for the bound script.
/// Carried on the route row so the orchestrator can partition the
/// route table without joining back to scripts on every refresh.
pub app_id: AppId,
/// Polymorphic owner (§11 tail): an **app** owns a concrete route; a
/// **group** owns a route TEMPLATE, expanded into every descendant app's
/// in-memory slice at rebuild. Exactly one is set (DB CHECK). Mirrors
/// `Trigger`. For an app route, `app_id` equals `scripts.app_id` for the
/// bound script — carried here so the orchestrator can partition the route
/// table without joining back to scripts on every refresh.
pub app_id: Option<AppId>,
pub group_id: Option<GroupId>,
pub script_id: ScriptId,
pub host_kind: HostKind,