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::read_only_repo_ctx;
20
21mod bindings {
22 wasmtime::component::bindgen!({
23 path: "wit",
24 world: "t0-plugin",
25 });
26}
27
28impl 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
44impl 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
83pub struct WasmT0 {
86 engine: Engine,
87 component: Component,
88 linker: Linker<HostState>,
89 repo_root: PathBuf,
90}
91
92impl WasmT0 {
93 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 #[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 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}