1use 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 imports: { default: async },
30 exports: { default: async },
31 });
32}
33
34impl 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
50impl 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
89pub struct WasmT0 {
92 engine: Engine,
93 component: Component,
94 linker: Linker<HostState>,
95 repo_root: PathBuf,
96}
97
98impl WasmT0 {
99 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 #[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 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}