Skip to main content

plugin_host/
t0.rs

1//! Wasm-бекенд поверхні `t0` (`wit/t0.wit`, world `t0-plugin`) — друга
2//! реалізація [`harness::pipeline::T0Fn`] (лише `plan`, доккоментар
3//! `crate` "T0 повертає edit-plan, хост застосовує": `PreparePlanFn`/
4//! `CommitPlanFn` сюди не входять, `n7n-llm-lib` тут узятий з
5//! `default-features = false`, `write_guard` цьому крейту недоступний за
6//! побудовою — той самий принцип, що вже застосовує `harness::registry`).
7
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use wasmtime::component::{Component, HasSelf, Linker, ResourceTable};
12use wasmtime::{Engine, Store};
13
14use harness::pipeline::T0Fn;
15use llm_lib::edit_plan::{AnchoredEdit, EditPlan, FileEditPlan};
16
17use crate::engine::new_engine;
18use crate::host_state::HostState;
19use crate::wasi_ctx::no_filesystem_ctx;
20
21mod bindings {
22    wasmtime::component::bindgen!({
23        path: "wit",
24        world: "t0-plugin",
25        // WASI p3 побудована на `component-model-async`, і синхронного
26        // лінкера в `wasmtime_wasi::p3` не існує — тож і згенерований
27        // виклик, і хостові імпорти мусять бути async (доккоментар
28        // `engine.rs`).
29        imports: { default: async },
30        exports: { default: async },
31    });
32}
33
34// Вкладені типи (`FileEditPlan`/`AnchoredEdit`/...) живуть під
35// `bindings::n7n::fix_deps::types` (БЕЗ `exports::`), не під
36// `bindings::exports::n7n::fix_deps::t0` — доккоментар `wit/t0.wit`,
37// секція про `import types;`, пояснює, чому bindgen! кладе їх саме туди.
38// Лише сам `EditPlan` (повернене значення `plan()`) лишається під
39// `exports::…::t0::EditPlan`, бо це тип експортованої функції.
40impl From<bindings::n7n::fix_deps::types::AnchoredEdit> for AnchoredEdit {
41    fn from(e: bindings::n7n::fix_deps::types::AnchoredEdit) -> Self {
42        AnchoredEdit {
43            anchor: e.anchor,
44            line: e.line as usize,
45            new_text: e.new_text,
46        }
47    }
48}
49
50/// Дзеркало `variant file-edit-plan` → [`FileEditPlan`] — доккоментар
51/// `detector.rs::From<...Violation> for Violation`: РЕАЛЬНИЙ конвертер
52/// `bindgen!`-типу, не ручне дзеркало. WIT `variant` мапиться на Rust
53/// `enum` з ОДНІЄЮ tuple-змінною на case (`Anchored(AnchoredFileEdit)`
54/// тощо) — той самий природний вибір, що вже коментує `wit/types.wit`
55/// ("WIT висловлює той самий вибір НАТИВНО через variant").
56impl From<bindings::n7n::fix_deps::types::FileEditPlan> for FileEditPlan {
57    fn from(plan: bindings::n7n::fix_deps::types::FileEditPlan) -> Self {
58        use bindings::n7n::fix_deps::types::FileEditPlan as Wit;
59        match plan {
60            Wit::Anchored(a) => FileEditPlan::Anchored {
61                path: PathBuf::from(a.path),
62                edits: a.edits.into_iter().map(AnchoredEdit::from).collect(),
63            },
64            Wit::Create(c) => FileEditPlan::Create {
65                path: PathBuf::from(c.path),
66                content: c.content,
67            },
68            Wit::Delete(d) => FileEditPlan::Delete {
69                path: PathBuf::from(d.path),
70            },
71        }
72    }
73}
74
75impl From<bindings::exports::n7n::fix_deps::t0::EditPlan> for EditPlan {
76    fn from(plan: bindings::exports::n7n::fix_deps::t0::EditPlan) -> Self {
77        EditPlan {
78            files: plan.files.into_iter().map(FileEditPlan::from).collect(),
79        }
80    }
81}
82
83impl bindings::n7n::fix_deps::host_fs::Host for HostState {
84    async fn read_file(&mut self, path: String) -> Result<String, String> {
85        std::fs::read_to_string(self.repo_root.join(path)).map_err(|e| e.to_string())
86    }
87}
88
89/// Wasm-бекенд одного `t0`-компонента — доккоментар
90/// [`crate::detector::WasmDetector`].
91pub struct WasmT0 {
92    engine: Engine,
93    component: Component,
94    linker: Linker<HostState>,
95    repo_root: PathBuf,
96}
97
98impl WasmT0 {
99    /// Доккоментар [`crate::detector::WasmDetector::from_file`].
100    pub fn from_file(
101        repo_root: impl Into<PathBuf>,
102        wasm_path: impl AsRef<Path>,
103    ) -> wasmtime::Result<Self> {
104        let engine = new_engine()?;
105        let component = Component::from_file(&engine, wasm_path.as_ref())?;
106        let mut linker = Linker::<HostState>::new(&engine);
107        wasmtime_wasi::p3::add_to_linker(&mut linker)?;
108        bindings::n7n::fix_deps::host_fs::add_to_linker::<_, HasSelf<HostState>>(
109            &mut linker,
110            |s| s,
111        )?;
112        Ok(Self {
113            engine,
114            component,
115            linker,
116            repo_root: repo_root.into(),
117        })
118    }
119
120    async fn call(&self) -> wasmtime::Result<EditPlan> {
121        let wasi = no_filesystem_ctx();
122        let mut store = Store::new(
123            &self.engine,
124            HostState {
125                wasi,
126                table: ResourceTable::new(),
127                repo_root: self.repo_root.clone(),
128            },
129        );
130        let instance =
131            bindings::T0Plugin::instantiate_async(&mut store, &self.component, &self.linker)
132                .await?;
133        let plan = instance.n7n_fix_deps_t0().call_plan(&mut store).await?;
134        Ok(EditPlan::from(plan))
135    }
136
137    /// Перетворює на [`T0Fn`]. `t0.wit` (`plan: func() -> edit-plan`) не
138    /// має каналу помилки — той самий брак, що вже несе Rust-сигнатура
139    /// (`Arc<dyn Fn() -> BoxFuture<'static, EditPlan>>`, без `Result`).
140    /// Інфраструктурний збій викликового шляху мапиться в
141    /// `EditPlan::empty()` — БЕЗПЕЧНИЙ сентинел "нічого не робимо", той
142    /// самий, яким порожній план уже означає "T0 нічого не зробив"
143    /// (доккоментар `edit_plan.rs`); guest, що не зміг порахувати план,
144    /// НІКОЛИ не мапиться на "план, що щось змінює".
145    #[must_use]
146    pub fn into_t0_fn(self) -> T0Fn {
147        let this = Arc::new(self);
148        Arc::new(move || {
149            let this = Arc::clone(&this);
150            Box::pin(async move {
151                this.call().await.unwrap_or_else(|err| {
152                    eprintln!("wasm-компонент t0: {err}");
153                    EditPlan::empty()
154                })
155            })
156        })
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    //! Доккоментар `detector.rs::tests` — той самий компіляційний доказ
163    //! без `.wasm`, тут для `edit-plan` (включно з вкладеними
164    //! `file-edit-plan`-варіантами, доккоментар `wit/t0.wit` про
165    //! `import types;`).
166    use super::*;
167
168    #[test]
169    fn bindgen_edit_plan_converts_all_three_file_edit_plan_variants() {
170        let wit_plan = bindings::exports::n7n::fix_deps::t0::EditPlan {
171            files: vec![
172                bindings::n7n::fix_deps::types::FileEditPlan::Anchored(
173                    bindings::n7n::fix_deps::types::AnchoredFileEdit {
174                        path: "src/a.rs".to_string(),
175                        edits: vec![bindings::n7n::fix_deps::types::AnchoredEdit {
176                            anchor: "1a2".to_string(),
177                            line: 3,
178                            new_text: Some("новий рядок".to_string()),
179                        }],
180                    },
181                ),
182                bindings::n7n::fix_deps::types::FileEditPlan::Create(
183                    bindings::n7n::fix_deps::types::CreateFileEdit {
184                        path: "src/b.rs".to_string(),
185                        content: "вміст\n".to_string(),
186                    },
187                ),
188                bindings::n7n::fix_deps::types::FileEditPlan::Delete(
189                    bindings::n7n::fix_deps::types::DeleteFileEdit {
190                        path: "src/c.rs".to_string(),
191                    },
192                ),
193            ],
194        };
195
196        let plan = EditPlan::from(wit_plan);
197
198        assert_eq!(
199            plan,
200            EditPlan {
201                files: vec![
202                    FileEditPlan::Anchored {
203                        path: PathBuf::from("src/a.rs"),
204                        edits: vec![AnchoredEdit {
205                            anchor: "1a2".to_string(),
206                            line: 3,
207                            new_text: Some("новий рядок".to_string()),
208                        }],
209                    },
210                    FileEditPlan::Create {
211                        path: PathBuf::from("src/b.rs"),
212                        content: "вміст\n".to_string(),
213                    },
214                    FileEditPlan::Delete {
215                        path: PathBuf::from("src/c.rs"),
216                    },
217                ],
218            }
219        );
220    }
221
222    #[test]
223    fn bindgen_anchored_edit_deletion_sentinel_survives_conversion() {
224        let wit_edit = bindings::n7n::fix_deps::types::AnchoredEdit {
225            anchor: "xyz".to_string(),
226            line: 7,
227            new_text: None,
228        };
229
230        assert_eq!(AnchoredEdit::from(wit_edit).new_text, None);
231    }
232}