n7n-plugin-host 0.1.0

wasmtime-бекенд для crate::registry::PluginRegistry (n7n-harness) — друга (wasm-компонентна) реалізація слотів detector/verify/ast_facts/t0, поруч з in-process-бекендом §3.12 пункту 16а
//! Wasm-бекенд поверхні `ast_facts` (`wit/ast-facts.wit`, world
//! `ast-facts-plugin`) — друга реалізація [`harness::registry::AstFactsFn`].

use std::path::{Path, PathBuf};
use std::sync::Arc;

use wasmtime::component::{Component, HasSelf, Linker, ResourceTable};
use wasmtime::{Engine, Store};

use harness::registry::AstFactsFn;

use crate::engine::new_engine;
use crate::host_state::HostState;
use crate::wasi_ctx::read_only_repo_ctx;

mod bindings {
    wasmtime::component::bindgen!({
        path: "../../wit",
        world: "ast-facts-plugin",
    });
}

impl bindings::n7n::fix_deps::host_fs::Host for HostState {
    fn read_file(&mut self, path: String) -> Result<String, String> {
        std::fs::read_to_string(self.repo_root.join(path)).map_err(|e| e.to_string())
    }
}

/// Wasm-бекенд одного `ast-facts`-компонента — доккоментар
/// [`crate::detector::WasmDetector`].
pub struct WasmAstFacts {
    engine: Engine,
    component: Component,
    linker: Linker<HostState>,
    repo_root: PathBuf,
}

impl WasmAstFacts {
    /// Доккоментар [`crate::detector::WasmDetector::from_file`].
    pub fn from_file(
        repo_root: impl Into<PathBuf>,
        wasm_path: impl AsRef<Path>,
    ) -> wasmtime::Result<Self> {
        let engine = new_engine()?;
        let component = Component::from_file(&engine, wasm_path.as_ref())?;
        let mut linker = Linker::<HostState>::new(&engine);
        wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
        bindings::n7n::fix_deps::host_fs::add_to_linker::<_, HasSelf<HostState>>(
            &mut linker,
            |s| s,
        )?;
        Ok(Self {
            engine,
            component,
            linker,
            repo_root: repo_root.into(),
        })
    }

    fn call(&self, path: &std::path::Path) -> wasmtime::Result<String> {
        let wasi = read_only_repo_ctx(&self.repo_root)?;
        let mut store = Store::new(
            &self.engine,
            HostState {
                wasi,
                table: ResourceTable::new(),
                repo_root: self.repo_root.clone(),
            },
        );
        let instance =
            bindings::AstFactsPlugin::instantiate(&mut store, &self.component, &self.linker)?;
        let facts = instance
            .n7n_fix_deps_ast_facts()
            .call_facts(&mut store, &path.to_string_lossy())?;
        Ok(facts)
    }

    /// Перетворює на [`AstFactsFn`]. `ast-facts.wit` (`facts: func(path:
    /// string) -> string`) не має каналу помилки взагалі — той самий брак,
    /// що вже несе Rust-сигнатура (`Arc<dyn Fn(PathBuf) ->
    /// BoxFuture<'static, String>>`, без `Option`/`Result`). Інфраструктурний
    /// збій викликового шляху (компонент не інстанціювався, трап) не має
    /// куди піти, крім stderr-логу й порожнього рядка — той самий сентинел,
    /// яким `attempt.rs` уже описує "інструмент недоступний" на рівні поля
    /// (тут — на рівні одного виклику).
    #[must_use]
    pub fn into_ast_facts_fn(self) -> AstFactsFn {
        let this = Arc::new(self);
        Arc::new(move |path: PathBuf| {
            let this = Arc::clone(&this);
            Box::pin(async move {
                this.call(&path).unwrap_or_else(|err| {
                    eprintln!("wasm-компонент ast-facts: {err}");
                    String::new()
                })
            })
        })
    }
}