-- §11.6 (v1.2 Hierarchies): group-collection registry — shared cross-app data. -- -- A row marks a collection NAME at a node as group-SHARED: every app in the -- owning group's subtree may read it (and, with an authenticated editor+, -- write it) via the explicit `kv::shared_collection("name")` SDK handle. Resolution is -- nearest-ancestor-group-wins, walking the reading app's chain — that ancestry -- walk is the isolation boundary (a foreign app's chain never contains the -- owning group, so the name simply does not resolve). See docs §11.6. -- -- This table holds only the MARKER (owner, name, kind) — the data itself lives -- in a per-kind storage table (`group_kv_entries`, 0053, for kind='kv'). It is -- pure declaration, structurally identical to an `extension_points` marker -- (0051). A marker is config, not code → ON DELETE CASCADE. -- -- Ownership is polymorphic (mirrors vars/secrets/scripts/extension_points): -- exactly one of (app_id, group_id) is set. MVP authoring is restricted to -- GROUP owners at the manifest layer (an app-declared shared collection is -- degenerate — only that app would read it); the polymorphic shape keeps the -- owner-generic reconcile path uniform with the other inheritable kinds. -- -- `kind` is the storage discriminator. Only 'kv' ships in the MVP; the column -- + CHECK exist now so docs/files/topics/queue slot in later without a -- migration, and so a future `docs` collection of the same name is a distinct -- row (the unique index covers `kind`). CREATE TABLE group_collections ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), group_id UUID REFERENCES groups(id) ON DELETE CASCADE, app_id UUID REFERENCES apps(id) ON DELETE CASCADE, CONSTRAINT group_collections_owner_exactly_one CHECK ((group_id IS NULL) <> (app_id IS NULL)), name TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'kv' CHECK (kind IN ('kv')), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- One marker per (owner, name, kind), case-insensitive. Partial because the -- owner is split across two nullable columns. CREATE UNIQUE INDEX group_collections_group_uidx ON group_collections (group_id, LOWER(name), kind) WHERE group_id IS NOT NULL; CREATE UNIQUE INDEX group_collections_app_uidx ON group_collections (app_id, LOWER(name), kind) WHERE app_id IS NOT NULL; -- Lookup indexes for the resolver's chain join + list-by-owner. CREATE INDEX group_collections_group_id_idx ON group_collections (group_id) WHERE group_id IS NOT NULL; CREATE INDEX group_collections_app_id_idx ON group_collections (app_id) WHERE app_id IS NOT NULL;