1use std::collections::BTreeMap;
18use std::path::{Path, PathBuf};
19
20use serde::{Deserialize, Serialize};
21
22use supercode_interchange::ontology::ArtifactFidelity;
23use supercode_interchange::world::codec::folder::OWNED_FILES;
24use supercode_interchange::world::codec::{
25 carry_unmodeled, from_hermes, from_openclaw, load_home, save_home, to_hermes, to_openclaw,
26 Flavor, LoadedHome, Refusal,
27};
28use supercode_interchange::world::World;
29
30use crate::Result;
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum HomeFlavor {
36 #[default]
38 Orchestrator,
39 Hermes,
41}
42
43impl From<HomeFlavor> for Flavor {
44 fn from(flavor: HomeFlavor) -> Self {
45 match flavor {
46 HomeFlavor::Orchestrator => Flavor::Orchestrator,
47 HomeFlavor::Hermes => Flavor::Hermes,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum SourceFlavor {
56 #[default]
58 Native,
59 Orchestrator,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum WorldHarness {
67 Hermes,
69 Openclaw,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct RefusalRow {
76 pub file: String,
78 pub reason: String,
80}
81
82impl From<&Refusal> for RefusalRow {
83 fn from(refusal: &Refusal) -> Self {
84 Self {
85 file: refusal.file.clone(),
86 reason: refusal.reason.clone(),
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct WorldRead {
94 pub world: World,
96 pub vault_keys: Vec<String>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct WorldSaved {
103 pub written: bool,
105 pub root: PathBuf,
107}
108
109#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
111pub struct WorldDecompiled {
112 pub written: Vec<ArtifactFidelity>,
114 pub refused: Vec<RefusalRow>,
116 #[serde(default, skip_serializing_if = "Vec::is_empty")]
118 pub notes: Vec<String>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub rows_byte: Option<usize>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub rows_emitted: Option<usize>,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct WorldImported {
131 pub world: World,
133 pub vault_keys: Vec<String>,
135 pub root: PathBuf,
137 pub carried: Vec<String>,
139}
140
141fn keys(vault: &BTreeMap<String, String>) -> Vec<String> {
142 vault.keys().cloned().collect()
143}
144
145fn repoint(world: &mut World, root: &Path) {
149 world.root = root.to_path_buf();
150 for (name, profile) in world.profiles.iter_mut() {
151 profile.dir = if name == "default" {
152 root.to_path_buf()
153 } else {
154 root.join("profiles").join(name)
155 };
156 }
157}
158
159pub fn load(root: &Path, flavor: HomeFlavor) -> Result<WorldRead> {
161 let loaded = load_home(root, flavor.into())?;
162 Ok(WorldRead {
163 vault_keys: keys(&loaded.vault),
164 world: loaded.world,
165 })
166}
167
168pub fn save(root: &Path, world: World, vault: BTreeMap<String, String>) -> Result<WorldSaved> {
175 let mut loaded = if root.is_dir() {
176 load_home(root, Flavor::Orchestrator)?
177 } else {
178 LoadedHome {
179 world: world.clone(),
180 vault: BTreeMap::new(),
181 io: BTreeMap::new(),
182 }
183 };
184 loaded.world = world;
185 repoint(&mut loaded.world, root);
186 loaded.vault.extend(vault);
187 save_home(&mut loaded, Some(root))?;
188 Ok(WorldSaved {
189 written: true,
190 root: root.to_path_buf(),
191 })
192}
193
194pub fn compile(from: WorldHarness, home: &Path) -> Result<WorldRead> {
196 Ok(match from {
197 WorldHarness::Hermes => {
198 let loaded = from_hermes(home)?;
199 WorldRead {
200 vault_keys: keys(&loaded.vault),
201 world: loaded.world,
202 }
203 }
204 WorldHarness::Openclaw => {
205 let loaded = from_openclaw(home)?;
206 WorldRead {
207 vault_keys: keys(&loaded.vault),
208 world: loaded.world,
209 }
210 }
211 })
212}
213
214pub fn decompile(
221 to: WorldHarness,
222 world: World,
223 source: &Path,
224 source_flavor: SourceFlavor,
225 dest: &Path,
226 vault: BTreeMap<String, String>,
227) -> Result<WorldDecompiled> {
228 Ok(match (to, source_flavor) {
229 (WorldHarness::Hermes, SourceFlavor::Orchestrator) => {
233 let mut loaded = load_home(source, HomeFlavor::Orchestrator.into())?;
234 loaded.world = world;
235 loaded.vault.extend(vault);
236 let report = to_hermes(&loaded, dest, None)?;
237 WorldDecompiled {
238 written: report.written,
239 refused: report.refused.iter().map(RefusalRow::from).collect(),
240 ..WorldDecompiled::default()
241 }
242 }
243 (WorldHarness::Openclaw, SourceFlavor::Orchestrator) => {
244 let loaded = supercode_interchange::world::codec::OpenclawLoaded::from_world(world, {
245 let mut v = load_home(source, HomeFlavor::Orchestrator.into())?.vault;
246 v.extend(vault);
247 v
248 });
249 let report = to_openclaw(&loaded, dest)?;
250 WorldDecompiled {
251 written: report.written,
252 refused: report.refused.iter().map(RefusalRow::from).collect(),
253 notes: report.notes,
254 rows_byte: Some(report.rows_byte),
255 rows_emitted: Some(report.rows_emitted),
256 }
257 }
258 (WorldHarness::Hermes, SourceFlavor::Native) => {
259 let mut loaded = from_hermes(source)?;
260 loaded.world = world;
261 loaded.vault.extend(vault);
262 let report = to_hermes(&loaded, dest, None)?;
263 WorldDecompiled {
264 written: report.written,
265 refused: report.refused.iter().map(RefusalRow::from).collect(),
266 ..WorldDecompiled::default()
267 }
268 }
269 (WorldHarness::Openclaw, SourceFlavor::Native) => {
270 let mut loaded = from_openclaw(source)?;
271 loaded.world = world;
272 loaded.vault.extend(vault);
273 let report = to_openclaw(&loaded, dest)?;
274 WorldDecompiled {
275 written: report.written,
276 refused: report.refused.iter().map(RefusalRow::from).collect(),
277 notes: report.notes,
278 rows_byte: Some(report.rows_byte),
279 rows_emitted: Some(report.rows_emitted),
280 }
281 }
282 })
283}
284
285pub fn import(from: WorldHarness, home: &Path, into: &Path) -> Result<WorldImported> {
293 let (world, vault, sources): (World, BTreeMap<String, String>, BTreeMap<String, PathBuf>) =
294 match from {
295 WorldHarness::Hermes => {
296 let loaded = from_hermes(home)?;
297 let sources = loaded
298 .io
299 .iter()
300 .filter_map(|(name, io)| Some((name.clone(), io.source_dir.clone()?)))
301 .collect();
302 (loaded.world, loaded.vault, sources)
303 }
304 WorldHarness::Openclaw => {
305 let loaded = from_openclaw(home)?;
306 let sources = loaded
309 .profiles
310 .iter()
311 .map(|(name, io)| {
312 let src = if name == "default" {
313 loaded.root.state_dir.clone()
314 } else {
315 io.source_dir.clone()
316 };
317 (name.clone(), src)
318 })
319 .collect();
320 (loaded.world, loaded.vault, sources)
321 }
322 };
323 let mut loaded = LoadedHome {
324 world,
325 vault,
326 io: BTreeMap::new(),
327 };
328 repoint(&mut loaded.world, into);
329 save_home(&mut loaded, Some(into))?;
330 let mut carried = Vec::new();
331 for (name, profile) in &loaded.world.profiles {
332 let Some(src) = sources.get(name) else {
333 continue;
334 };
335 let files: Vec<String> = profile
340 .residue
341 .files
342 .iter()
343 .filter(|rel| !OWNED_FILES.contains(&rel.as_str()))
344 .cloned()
345 .collect();
346 for rel in carry_unmodeled(&files, src, &profile.dir)? {
347 carried.push(if name == "default" {
348 rel
349 } else {
350 format!("profiles/{name}/{rel}")
351 });
352 }
353 }
354 Ok(WorldImported {
355 vault_keys: keys(&loaded.vault),
356 world: loaded.world,
357 root: into.to_path_buf(),
358 carried,
359 })
360}
361
362pub fn export(to: WorldHarness, root: &Path, dest: &Path) -> Result<WorldDecompiled> {
369 let loaded = load_home(root, Flavor::Orchestrator)?;
370 decompile(
371 to,
372 loaded.world,
373 root,
374 SourceFlavor::Orchestrator,
375 dest,
376 loaded.vault,
377 )
378}