diff --git a/CLAUDE.md b/CLAUDE.md index c33d0b6..7822b2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,9 +10,9 @@ Authoritative design: [serverless_cloud_blueprint.md](serverless_cloud_blueprint **v1.1.x — SDK foundation + services — is complete.** The SDK shape (handle pattern, `::` namespaces, `Services`/`SdkCallCx`; see [docs/sdk-shape.md](docs/sdk-shape.md), stdlib at [docs/stdlib-reference.md](docs/stdlib-reference.md)) fixed in v1.1.0, then KV, docs, modules, HTTP, cron, files, pub/sub, email, users, and durable queues + `invoke()` filled it in through **v1.1.9** — blueprint §12 has the table. Earlier groundwork: blueprint Phase 3 (admin auth, multi-app scoping, Phase 3.5 capability gating — `manager-core::authz::{can, require, Capability}`, migration `0006_users_authz.sql`). -**Current focus: v1.2 _Hierarchies_ — groups + the declarative project tool** ([docs/design/groups-and-project-tool.md](docs/design/groups-and-project-tool.md)). That doc's §11 uses its own **Phase 1–6 numbering, distinct from the blueprint product-phase numbering above — do not conflate them** (its "Phase 3" = group-inherited config, not admin auth). Implemented on `feat/groups-*` branches: §11 Phase 1 (declarative `pic plan`/`apply`/`prune` + env overlays), Phase 2 (single-parent groups tree + hierarchy-aware RBAC), Phase 3 (group-inherited, env-scoped `vars` + secrets resolved **live** via a recursive CTE — no materialized cache). Next: group-owned scripts/modules (§11 Phase 4) and the project tool mapping onto groups (§11 Phase 5). +**Current focus: v1.2 _Hierarchies_ — groups + the declarative project tool** ([docs/design/groups-and-project-tool.md](docs/design/groups-and-project-tool.md)). That doc's §11 uses its own **Phase 1–6 numbering, distinct from the blueprint product-phase numbering above — do not conflate them** (its "Phase 3" = group-inherited config, not admin auth). Implemented on `feat/groups-*` branches: §11 Phase 1 (declarative `pic plan`/`apply`/`prune` + env overlays), Phase 2 (single-parent groups tree + hierarchy-aware RBAC), Phase 3 (group-inherited, env-scoped `vars` + secrets resolved **live** via a recursive CTE — no materialized cache), Phase 4-lite (group-owned **endpoint** scripts: `scripts` polymorphic owner in `0050_group_scripts.sql`, `get_by_name_inherited`/`is_invocable_by_app` chain resolution, inherited `invoke()` + declarative route/trigger binding — all **live**, no body materialization; group modules + the lexical import resolver are Phase 4b). Next: the project tool mapping onto groups (§11 Phase 5). -**Data-model invariant:** app-owned data-plane tables (KV, docs, files, …) start with `app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE`; the group-inheritable _config_ tables (`vars`, `secrets`) instead carry a **polymorphic owner** — nullable `group_id` and `app_id` with an exactly-one CHECK and per-owner partial-unique indexes. Every Rhai SDK call resolves its app from `cx.app_id`, never a script-passed arg (the cross-app isolation boundary). +**Data-model invariant:** app-owned data-plane tables (KV, docs, files, …) start with `app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE`; the group-inheritable tables — _config_ (`vars`, `secrets`) and now group-owned _code_ (`scripts`, `0050`) — instead carry a **polymorphic owner**: nullable `group_id` and `app_id` with an exactly-one CHECK and per-owner partial-unique indexes (config is `ON DELETE CASCADE`, scripts `RESTRICT` — code is not data). Inheritance resolves **live** down `apps.group_id → groups.parent_id` via `CHAIN_LEVELS_CTE` (no materialized view); nearest-owner-wins with an app's own row shadowing the inherited one (CoW). Every Rhai SDK call resolves its app from `cx.app_id`, never a script-passed arg, and a group script always runs under the *inheriting* app's `cx.app_id` (the cross-app isolation boundary). ## Three-Service Architecture diff --git a/docs/design/groups-and-project-tool.md b/docs/design/groups-and-project-tool.md index bcc7af6..f2f2cb5 100644 --- a/docs/design/groups-and-project-tool.md +++ b/docs/design/groups-and-project-tool.md @@ -440,6 +440,10 @@ Two distinct constraints: > **deferred plan for *script-body* inheritance (Phase 4)** — where a hot per-request path may > justify a cache — and as a future optimization for config if a deep tree ever demands it. Read the > rest of §5.1 as "the materialization design, if/when we need it," not as Phase 3's runtime. + > **Update — Phase 4-lite (§11.4 status) also shipped live, not materialized:** group scripts are + > referenced by id on the runtime hot path and resolved by name only at bind/invoke time, so even + > script inheritance needed no materialized view or invalidation fan-out. The materialization design + > now stands purely as a future optimization, gated on a *measured* cost. - manager-core **resolves-at-write into a materialized per-app effective view** (§3 rule applied: sparse merge, env filter, proximity, CoW, `enabled`). - The orchestrator/executor serve from that view, **keyed by `app_id` + a generation/version**. The @@ -942,6 +946,44 @@ Resolved items now live inline next to their topic. What genuinely remains: > apply reconciliation (Phase 5 — Phase 3 ships CRUD API + CLI), and materialization (above). 4. **Group-inherited scripts/modules.** CoW overrides; the **scope-aware module/import resolver + extension points** (§5.5); cache-invalidation fan-out hardening; versioning/pinning if needed. + + > **Status (Phase 4-lite): ✅ shipped — group-owned ENDPOINT scripts, live-resolved (no + > materialization).** Like Phase 3's config, group scripts resolve **live**, not via a cache: a + > script is referenced by id on the runtime hot path (routes/triggers store the resolved id; the + > orchestrator's per-app route trie already scopes dispatch to one app and never reads + > `script.app_id`), and by **name** down the chain only at bind/invoke time. No materialized + > per-app body view, no generation counter, no fan-out invalidation — so §11.4's + > "cache-invalidation fan-out hardening" is **moot for Phase 4-lite** (revisit only if a measured + > hot-path cost appears). + > + > Shipped surface: + > - **Schema:** `0050_group_scripts.sql` makes `scripts` polymorphic-owned (`group_id` XOR + > `app_id`, `scripts_owner_exactly_one` CHECK, per-owner partial-unique `LOWER(name)` indexes), + > `ON DELETE RESTRICT` (code is not data — a group can't be deleted out from under its scripts). + > `Script.app_id` became `Option` + `group_id`; the cross-app isolation backstops + > (dispatcher, invoke, trigger-bind, orchestrator `/execute/{id}`) generalized from + > `app_id == row.app_id` to **chain-membership**, fail-closed for app-owned (zero-cost in-memory + > fast path; only a group candidate pays a chain query). + > - **Resolver (reuses `CHAIN_LEVELS_CTE`):** `get_by_name_inherited(app_id, name)` (nearest-owner + > wins; app's own script shadows the inherited group one — CoW) and `is_invocable_by_app(script_id, + > app_id)` (the isolation predicate lifted to the hierarchy; sibling apps' *own* scripts are NOT + > cross-invocable — only group scripts inherit). + > - **Admin + CLI:** `GroupScripts{Read,Write}` caps (viewer+/editor+ on the group, resolved via the + > group-ancestor walk); `POST/GET /groups/{id}/scripts` (endpoint-only, self-contained — modules + + > imports rejected); the by-id `/scripts/{id}` handlers are owner-polymorphic; `pic scripts ls + > --group` / `deploy --group` (create-or-update; `--app`/`--group` mutually exclusive). + > - **`invoke("name", …)`** resolves inherited (a script runs a shared group endpoint; it always + > executes under the *caller's* app context, never the owner's). + > - **Declarative apply:** a manifest route/trigger `script = "name"` resolves to an inherited group + > endpoint (`resolve_inherited_targets` → `name_to_id`), with the diff resolving the group-bound + > route's id → name so **re-apply is idempotent**. + > + > Deliberate Phase-4-lite limits (deferred to Phase 4b): group **modules** + the origin-aware + > (lexical) **import** resolver (§5.5) — group scripts must be self-contained; **invoke-by-id** stays + > app-scoped (inheritance is by-name only); CoW is **redefine-in-app + re-apply** (no live + > auto-rebinding). Sharp edge to track: `routes.script_id` is `ON DELETE CASCADE`, so deleting a + > group script removes descendant apps' bound routes (within group-editor authority; the *group* + > delete itself stays `RESTRICT`). 5. **Project tool maps onto groups.** Nested manifests, attach point, single-owner, server-computed tree plan, per-env approval gating. 6. **(Much later) group-level collections/topics** — the v1.3 cross-app data-sharing problem, with a