docs: record Phase 4-lite (group-owned scripts) as shipped, live-resolved
Mark §11.4 ✅ shipped with the live-resolution decision (no body materialization / invalidation fan-out, mirroring Phase 3's config), the shipped surface, and the deliberate Phase-4b deferrals (group modules + lexical import resolver, invoke-by-id, live auto-rebinding). Note the sharp edge: routes.script_id ON DELETE CASCADE means deleting a group script drops descendant routes. Update the §5.1 materialization box and CLAUDE.md (current-focus + the data-model invariant now covers group-owned scripts). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<AppId>` + `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
|
||||
|
||||
Reference in New Issue
Block a user