//! `PostgresModuleSource` — the Postgres-backed `ModuleSource` impl. //! //! `resolve` is **lexical** (Phase 4b): a module is looked up by walking the //! group chain rooted at the **importing script's defining node** (`origin`), //! nearest-owner-wins — *not* the inheriting app's effective view. An `App` //! origin walks `app → its group → ancestors` (reusing [`CHAIN_LEVELS_CTE`]); //! a `Group` origin walks `group → ancestors` ([`GROUP_CHAIN_LEVELS_CTE`]) and //! never sees app-owned modules below it (the trust boundary). //! //! `resolve_policy` adds §5.5 **extension points**: the nearest declaration of //! a name on `origin`'s chain decides lexical (concrete module) vs **dynamic** //! (an extension-point marker → resolve against the inheriting app so it can //! override, falling back to a default body up-chain). EP wins a depth tie. //! //! The resolver lives in `executor-core` and consumes this trait through the //! `Services` bundle, so manager-core stays the only crate that touches Postgres. use async_trait::async_trait; use chrono::{DateTime, Utc}; use picloud_shared::{ AppId, ModuleResolution, ModuleScript, ModuleSource, ModuleSourceError, ScriptOwner, }; use sqlx::PgPool; use crate::config_resolver::{CHAIN_LEVELS_CTE, GROUP_CHAIN_LEVELS_CTE}; pub struct PostgresModuleSource { pool: PgPool, } impl PostgresModuleSource { #[must_use] pub fn new(pool: PgPool) -> Self { Self { pool } } } #[derive(sqlx::FromRow)] struct ModuleRow { id: uuid::Uuid, app_id: Option, group_id: Option, name: String, source: String, updated_at: DateTime, } impl From for ModuleScript { fn from(r: ModuleRow) -> Self { Self { script_id: r.id.into(), app_id: r.app_id.map(Into::into), group_id: r.group_id.map(Into::into), name: r.name, source: r.source, updated_at: r.updated_at, } } } #[async_trait] impl ModuleSource for PostgresModuleSource { async fn resolve( &self, origin: ScriptOwner, name: &str, ) -> Result, ModuleSourceError> { // Lexical lookup: JOIN module scripts against the chain rooted at // `origin`, take the nearest level (lowest depth). The `kind = // 'module'` filter + the CHECK constraint guarantee endpoint // scripts are never importable. Case-insensitive to match the // per-owner `LOWER(name)` partial-unique indexes (0050). `origin` // is a trusted dispatch-derived value, so the chain it roots is // the script's true ancestry — the cross-app isolation boundary // is preserved (a group origin can't reach an app's modules). let query = match origin { ScriptOwner::App(_) => format!( "{CHAIN_LEVELS_CTE} \ SELECT s.id, s.app_id, s.group_id, s.name, s.source, s.updated_at \ FROM scripts s \ JOIN chain c ON (s.app_id = c.app_owner OR s.group_id = c.group_owner) \ WHERE s.kind = 'module' AND LOWER(s.name) = LOWER($2) \ ORDER BY c.depth ASC LIMIT 1", ), ScriptOwner::Group(_) => format!( "{GROUP_CHAIN_LEVELS_CTE} \ SELECT s.id, s.app_id, s.group_id, s.name, s.source, s.updated_at \ FROM scripts s \ JOIN chain c ON s.group_id = c.group_owner \ WHERE s.kind = 'module' AND LOWER(s.name) = LOWER($2) \ ORDER BY c.depth ASC LIMIT 1", ), }; let root = match origin { ScriptOwner::App(a) => a.into_inner(), ScriptOwner::Group(g) => g.into_inner(), }; let row: Option = sqlx::query_as(&query) .bind(root) .bind(name) .fetch_optional(&self.pool) .await .map_err(|e| ModuleSourceError::Backend(e.to_string()))?; Ok(row.map(Into::into)) } async fn resolve_policy( &self, origin: ScriptOwner, inheriting_app: AppId, name: &str, ) -> Result { // §5.5 nearest-declaration-kind-wins. One round trip: the min chain // depth at which `name` is declared as an extension-point marker vs a // concrete module, walking up `origin`'s chain. The EP wins a tie (a // marker + its co-located default body live at the same node). let query = match origin { ScriptOwner::App(_) => format!( "{CHAIN_LEVELS_CTE} \ SELECT \ (SELECT MIN(c.depth) FROM extension_points e \ JOIN chain c ON (e.app_id = c.app_owner OR e.group_id = c.group_owner) \ WHERE LOWER(e.name) = LOWER($2)) AS ep_depth, \ (SELECT MIN(c.depth) FROM scripts s \ JOIN chain c ON (s.app_id = c.app_owner OR s.group_id = c.group_owner) \ WHERE s.kind = 'module' AND LOWER(s.name) = LOWER($2)) AS mod_depth", ), ScriptOwner::Group(_) => format!( "{GROUP_CHAIN_LEVELS_CTE} \ SELECT \ (SELECT MIN(c.depth) FROM extension_points e \ JOIN chain c ON e.group_id = c.group_owner \ WHERE LOWER(e.name) = LOWER($2)) AS ep_depth, \ (SELECT MIN(c.depth) FROM scripts s \ JOIN chain c ON s.group_id = c.group_owner \ WHERE s.kind = 'module' AND LOWER(s.name) = LOWER($2)) AS mod_depth", ), }; let root = match origin { ScriptOwner::App(a) => a.into_inner(), ScriptOwner::Group(g) => g.into_inner(), }; let (ep_depth, mod_depth): (Option, Option) = sqlx::query_as(&query) .bind(root) .bind(name) .fetch_one(&self.pool) .await .map_err(|e| ModuleSourceError::Backend(e.to_string()))?; // EP wins when it is declared and is no farther than the nearest // concrete module (tie → EP). Then resolve dynamically against the // inheriting app (its own override, else the default body up-chain). let ep_wins = match (ep_depth, mod_depth) { (Some(ep), Some(m)) => ep <= m, (Some(_), None) => true, _ => false, }; if ep_wins { return Ok( match self.resolve(ScriptOwner::App(inheriting_app), name).await? { Some(m) => ModuleResolution::Module(m), None => ModuleResolution::NoProvider, }, ); } if mod_depth.is_some() { // Concrete module nearest → lexical, exactly as Phase 4b. return Ok(match self.resolve(origin, name).await? { Some(m) => ModuleResolution::Module(m), None => ModuleResolution::NotFound, }); } Ok(ModuleResolution::NotFound) } }