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:
@@ -76,7 +76,9 @@ pub use inbox::{
|
||||
pub use invoke::{InvokeError, InvokeService, InvokeTarget, NoopInvokeService, ResolvedScript};
|
||||
pub use kv::{KvError, KvListPage, KvService, NoopKvService};
|
||||
pub use log_sink::{ExecutionLogSink, LogSinkError};
|
||||
pub use modules::{ModuleScript, ModuleSource, ModuleSourceError, NoopModuleSource};
|
||||
pub use modules::{
|
||||
ModuleResolution, ModuleScript, ModuleSource, ModuleSourceError, NoopModuleSource,
|
||||
};
|
||||
pub use outbox_writer::{HttpDispatchPayload, NewHttpOutbox, OutboxWriter, OutboxWriterError};
|
||||
pub use pubsub::{
|
||||
topic_matches, validate_topic_pattern, NoopPubsubService, PubsubError, PubsubService,
|
||||
|
||||
@@ -62,6 +62,19 @@ impl ModuleScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of [`ModuleSource::resolve_policy`] — the §5.5 lexical-vs-dynamic
|
||||
/// decision the resolver acts on.
|
||||
#[derive(Debug)]
|
||||
pub enum ModuleResolution {
|
||||
/// Bind this module (lexical, or an extension point's resolved provider).
|
||||
Module(ModuleScript),
|
||||
/// The name is an extension point, but no provider exists for the
|
||||
/// inheriting app (no app override and no default body) — a hard error.
|
||||
NoProvider,
|
||||
/// No declaration of the name anywhere on the chain → module-not-found.
|
||||
NotFound,
|
||||
}
|
||||
|
||||
/// Lookup contract used by `PicloudModuleResolver`. `resolve` walks the
|
||||
/// module set up the chain rooted at `origin` (the importing script's
|
||||
/// defining node), nearest-owner-wins. The `origin` is trusted
|
||||
@@ -80,6 +93,28 @@ pub trait ModuleSource: Send + Sync {
|
||||
origin: ScriptOwner,
|
||||
name: &str,
|
||||
) -> Result<Option<ModuleScript>, ModuleSourceError>;
|
||||
|
||||
/// §5.5 extension-point-aware resolution. Decides lexical vs dynamic by
|
||||
/// the **nearest declaration** of `name` walking up `origin`'s chain: a
|
||||
/// concrete module → lexical (`resolve(origin, …)`); an extension-point
|
||||
/// marker → dynamic, resolved against the **inheriting app**
|
||||
/// (`resolve(App(inheriting_app), …)`) so the app can override, falling
|
||||
/// back to the default body up-chain; the EP wins on a depth tie.
|
||||
///
|
||||
/// The default impl is lexical-only (no extension points) — the Postgres
|
||||
/// source overrides it. Test fakes that don't exercise EPs inherit this.
|
||||
async fn resolve_policy(
|
||||
&self,
|
||||
origin: ScriptOwner,
|
||||
inheriting_app: AppId,
|
||||
name: &str,
|
||||
) -> Result<ModuleResolution, ModuleSourceError> {
|
||||
let _ = inheriting_app;
|
||||
Ok(match self.resolve(origin, name).await? {
|
||||
Some(m) => ModuleResolution::Module(m),
|
||||
None => ModuleResolution::NotFound,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Failure modes surfaced from `ModuleSource::resolve`. "Not found" is
|
||||
|
||||
Reference in New Issue
Block a user