n7n-plugin-host 0.1.6

wasmtime-бекенд для crate::registry::PluginRegistry (n7n-harness) — друга (wasm-компонентна) реалізація слотів detector/verify/ast_facts/t0, поруч з in-process-бекендом §3.12 пункту 16а
Documentation
//! Wasm-бекенд поверхні `verify` (`wit/verify.wit`, world `verify-plugin`)
//! — друга реалізація [`harness::registry::VerifyFn`].
//!
//! `wit/verify.wit` document-ує, що ЦЕ покриває лише 56/58 виміряних
//! native-концернів `rules-core` — 2 (`kubeconform`/`kubescape`) спавнять
//! зовнішній бінарник і wasm-компонентом узагалі не можуть бути (guest не
//! спавнить процесів, доккоментар `crate` "жодного спавну процесів"); для
//! них `verify` лишається host-інʼєкцією через in-process бекенд
//! `harness::registry`, не через цей файл.

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

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

use harness::registry::VerifyFn;
use llm_lib::attempt::VerifyReport;

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: "verify-plugin",
    });
}

/// Дзеркало `record verify-report` → [`VerifyReport`] — доккоментар
/// `detector.rs::From<...Violation> for Violation` пояснює, чому це
/// РЕАЛЬНИЙ конвертер `bindgen!`-типу, не ручне дзеркало.
impl From<bindings::exports::n7n::fix_deps::verify::VerifyReport> for VerifyReport {
    fn from(r: bindings::exports::n7n::fix_deps::verify::VerifyReport) -> Self {
        VerifyReport {
            ok: r.ok,
            output: r.output,
            infra_error: r.infra_error,
        }
    }
}

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-бекенд одного `verify`-компонента — доккоментар [`crate::detector::WasmDetector`]
/// пояснює кешування `Component`/`Engine`/`Linker` проти "інстанс на
/// спробу" для `Instance`.
pub struct WasmVerify {
    engine: Engine,
    component: Component,
    linker: Linker<HostState>,
    repo_root: PathBuf,
}

impl WasmVerify {
    /// Доккоментар [`crate::detector::WasmDetector::from_file`] — та сама
    /// побудова `Engine`/`Component`/`Linker`, лише інший `world`.
    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) -> wasmtime::Result<VerifyReport> {
        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::VerifyPlugin::instantiate(&mut store, &self.component, &self.linker)?;
        let report = instance.n7n_fix_deps_verify().call_run(&mut store)?;
        Ok(VerifyReport::from(report))
    }

    /// Перетворює на [`VerifyFn`]. `verify.wit` (`run: func() ->
    /// verify-report`, без `result`) не має каналу для "сам виклик упав" —
    /// той самий брак, що вже несе Rust-сигнатура
    /// (`Arc<dyn Fn() -> BoxFuture<'static, VerifyReport>>`, без `Result`).
    /// Інфраструктурний збій викликового шляху (інстанціювання, трап)
    /// мапиться в `infra_error: true` — рівно те поле, яке `attempt.rs`
    /// документує як "не результат «червоно»": rig списує хід завжди,
    /// облік такого випадку — наш.
    #[must_use]
    pub fn into_verify_fn(self) -> VerifyFn {
        let this = Arc::new(self);
        Arc::new(move || {
            let this = Arc::clone(&this);
            Box::pin(async move {
                this.call().unwrap_or_else(|err| VerifyReport {
                    ok: false,
                    output: format!("wasm-компонент verify: {err}"),
                    infra_error: true,
                })
            })
        })
    }
}

#[cfg(test)]
mod tests {
    //! Доккоментар `detector.rs::tests` — той самий компіляційний доказ
    //! без `.wasm`, тут для `verify-report`.
    use super::*;

    #[test]
    fn bindgen_verify_report_converts_to_llm_lib_verify_report_field_by_field() {
        let wit_report = bindings::exports::n7n::fix_deps::verify::VerifyReport {
            ok: false,
            output: "діагностика".to_string(),
            infra_error: true,
        };

        let report = VerifyReport::from(wit_report);

        assert!(!report.ok);
        assert_eq!(report.output, "діагностика");
        assert!(report.infra_error);
    }
}