-- Phase 4b+1 (v1.2 Hierarchies): extension points (§5.5). -- -- An extension point opts a MODULE NAME into dynamic, per-tenant resolution. -- Normally a group/parent script's `import "x"` resolves sealed-lexically -- against the importer's own defining node — a leaf app cannot shadow it -- (the trust boundary). When a node declares `x` an extension point, an -- importer whose defining node is on that node's chain instead resolves `x` -- against the INHERITING app's effective view, so each descendant app may -- supply its own `x`. Only the declaring (parent) node can open the hole; a -- descendant cannot make one of its parent's sealed imports dynamic, because -- the declaration must live on the importer's own ancestry. -- -- Owner-polymorphic exactly like vars (0048): exactly one of group_id/app_id. -- Optional `default_script_id` is the fallback module body used when the -- inheriting app provides no module of `name`. CREATE TABLE extension_points ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Polymorphic owner: exactly one FK set (real FKs so ON DELETE CASCADE -- works per owner kind and a dangling owner is impossible). group_id UUID REFERENCES groups(id) ON DELETE CASCADE, app_id UUID REFERENCES apps(id) ON DELETE CASCADE, CONSTRAINT extension_points_owner_exactly_one CHECK ((group_id IS NULL) <> (app_id IS NULL)), name TEXT NOT NULL, -- Optional fallback module (a module owned by the declaring node). SET -- NULL on delete so deleting the module can never block — the fallback is -- then absent and a non-providing app errors at import time (the next plan -- shows the now-null default as an Update). A valid manifest apply can't -- reach this: validation rejects a kept ext point whose default names a -- module not in the bundle. default_script_id UUID REFERENCES scripts(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- One declaration per (owner, name), case-insensitive to match the module -- name indexes (0050). Partial because the owner is split across two columns. CREATE UNIQUE INDEX extension_points_group_uidx ON extension_points (group_id, LOWER(name)) WHERE group_id IS NOT NULL; CREATE UNIQUE INDEX extension_points_app_uidx ON extension_points (app_id, LOWER(name)) WHERE app_id IS NOT NULL; CREATE INDEX extension_points_group_id_idx ON extension_points (group_id) WHERE group_id IS NOT NULL; CREATE INDEX extension_points_app_id_idx ON extension_points (app_id) WHERE app_id IS NOT NULL; CREATE INDEX extension_points_default_script_idx ON extension_points (default_script_id) WHERE default_script_id IS NOT NULL;