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