feat(modules): extension-point-aware resolver policy (§5.5 C2)
Add the runtime heart of extension points: `ModuleSource::resolve_policy`
decides lexical vs dynamic resolution by the NEAREST declaration of a name
walking up the importing origin's chain — a concrete module → lexical (seal
to origin, Phase 4b); an extension-point marker → dynamic, resolved against
the inheriting app (`cx.app_id`) so the app can override, falling back to the
default body up-chain; the EP wins a depth tie.
- shared: `ModuleResolution { Module | NoProvider | NotFound }` + a
`resolve_policy(origin, inheriting_app, name)` trait method with a
lexical-only default impl (existing fakes inherit it unchanged).
- PostgresModuleSource: one-round-trip nearest-declaration query (min EP depth
vs min module depth over the chain CTEs), then the lexical/dynamic branch.
- module_resolver: calls `resolve_policy(origin, cx.app_id, path)`; maps
NoProvider → a clear "extension point has no provider" error, NotFound →
module-not-found. Cache + AST-source sealing unchanged.
- executor-core tests: a policy fake + 3 cases (app override binds dynamically,
no-provider errors, non-EP stays lexical).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -377,17 +377,40 @@ impl ModuleResolver for PicloudModuleResolver {
|
||||
let origin = importer_source
|
||||
.and_then(decode_origin)
|
||||
.unwrap_or(self.default_origin);
|
||||
let lookup_result: Result<Option<picloud_shared::ModuleScript>, ModuleSourceError> =
|
||||
tokio::task::block_in_place(|| handle.block_on(self.source.resolve(origin, path)));
|
||||
// §5.5 policy resolution: the source decides lexical (seal to `origin`)
|
||||
// vs dynamic (an extension point resolves against the inheriting app,
|
||||
// `cx.app_id`). The result is already the concrete module to bind, or a
|
||||
// terminal NoProvider/NotFound.
|
||||
let lookup_result: Result<picloud_shared::ModuleResolution, ModuleSourceError> =
|
||||
tokio::task::block_in_place(|| {
|
||||
handle.block_on(self.source.resolve_policy(origin, self.cx.app_id, path))
|
||||
});
|
||||
|
||||
let module_row = match lookup_result {
|
||||
Ok(Some(m)) => m,
|
||||
Ok(None) => {
|
||||
Ok(picloud_shared::ModuleResolution::Module(m)) => m,
|
||||
Ok(picloud_shared::ModuleResolution::NotFound) => {
|
||||
return Err(Box::new(EvalAltResult::ErrorModuleNotFound(
|
||||
path.to_string(),
|
||||
pos,
|
||||
)));
|
||||
}
|
||||
Ok(picloud_shared::ModuleResolution::NoProvider) => {
|
||||
// §5.5: an extension point with no provider for this app is a
|
||||
// hard failure (the app must supply the module or a default
|
||||
// body must exist up-chain). Distinct, actionable message.
|
||||
return Err(Box::new(EvalAltResult::ErrorInModule(
|
||||
path.to_string(),
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!(
|
||||
"extension point {path:?} has no provider for this app — \
|
||||
define a module named {path:?} or a default body up-chain"
|
||||
)
|
||||
.into(),
|
||||
pos,
|
||||
)),
|
||||
pos,
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
// v1.1.4 §10a: redact the backend error before it
|
||||
// reaches a script. In public-HTTP context (principal:
|
||||
|
||||
Reference in New Issue
Block a user