1use std::collections::BTreeMap;
18use std::path::{Path, PathBuf};
19
20use serde::{Deserialize, Serialize};
21
22use supercode_interchange::ontology::ArtifactFidelity;
23use supercode_interchange::orchestration::codec::folder::OWNED_FILES;
24use supercode_interchange::orchestration::codec::{
25 carry_unmodeled, from_hermes, from_openclaw, load_home, save_home, to_hermes, to_openclaw,
26 Flavor, LoadedHome, Refusal,
27};
28use supercode_interchange::orchestration::Orchestration;
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 OrchestrationHarness {
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 OrchestrationRead {
94 pub orchestration: Orchestration,
96 pub vault_keys: Vec<String>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct OrchestrationSaved {
103 pub written: bool,
105 pub root: PathBuf,
107}
108
109#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
111pub struct OrchestrationDecompiled {
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 OrchestrationImported {
131 pub orchestration: Orchestration,
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(orchestration: &mut Orchestration, root: &Path) {
149 orchestration.root = root.to_path_buf();
150 for (name, profile) in orchestration.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
159fn patiently<T>(
164 read: impl Fn() -> std::result::Result<T, supercode_interchange::InterchangeError>,
165) -> std::result::Result<T, supercode_interchange::InterchangeError> {
166 match read() {
167 Err(supercode_interchange::InterchangeError::Io(ref io))
168 if io.kind() == std::io::ErrorKind::NotFound =>
169 {
170 std::thread::sleep(std::time::Duration::from_millis(150));
171 read()
172 }
173 other => other,
174 }
175}
176
177pub fn load(root: &Path, flavor: HomeFlavor) -> Result<OrchestrationRead> {
179 let loaded = patiently(|| load_home(root, flavor.into()))?;
180 Ok(OrchestrationRead {
181 vault_keys: keys(&loaded.vault),
182 orchestration: loaded.orchestration,
183 })
184}
185
186pub fn save(
193 root: &Path,
194 orchestration: Orchestration,
195 vault: BTreeMap<String, String>,
196) -> Result<OrchestrationSaved> {
197 let mut loaded = if root.is_dir() {
198 load_home(root, Flavor::Orchestrator)?
199 } else {
200 LoadedHome {
201 orchestration: orchestration.clone(),
202 vault: BTreeMap::new(),
203 io: BTreeMap::new(),
204 }
205 };
206 loaded.orchestration = orchestration;
207 repoint(&mut loaded.orchestration, root);
208 loaded.vault.extend(vault);
209 save_home(&mut loaded, Some(root))?;
210 Ok(OrchestrationSaved {
211 written: true,
212 root: root.to_path_buf(),
213 })
214}
215
216pub fn compile(from: OrchestrationHarness, home: &Path) -> Result<OrchestrationRead> {
218 Ok(match from {
219 OrchestrationHarness::Hermes => {
220 let loaded = patiently(|| from_hermes(home))?;
221 OrchestrationRead {
222 vault_keys: keys(&loaded.vault),
223 orchestration: loaded.orchestration,
224 }
225 }
226 OrchestrationHarness::Openclaw => {
227 let loaded = from_openclaw(home)?;
228 OrchestrationRead {
229 vault_keys: keys(&loaded.vault),
230 orchestration: loaded.orchestration,
231 }
232 }
233 })
234}
235
236pub fn decompile(
243 to: OrchestrationHarness,
244 orchestration: Orchestration,
245 source: &Path,
246 source_flavor: SourceFlavor,
247 dest: &Path,
248 vault: BTreeMap<String, String>,
249) -> Result<OrchestrationDecompiled> {
250 Ok(match (to, source_flavor) {
251 (OrchestrationHarness::Hermes, SourceFlavor::Orchestrator) => {
255 let mut loaded = load_home(source, HomeFlavor::Orchestrator.into())?;
256 loaded.orchestration = orchestration;
257 loaded.vault.extend(vault);
258 let report = to_hermes(&loaded, dest, None)?;
259 OrchestrationDecompiled {
260 written: report.written,
261 refused: report.refused.iter().map(RefusalRow::from).collect(),
262 ..OrchestrationDecompiled::default()
263 }
264 }
265 (OrchestrationHarness::Openclaw, SourceFlavor::Orchestrator) => {
266 let loaded =
267 supercode_interchange::orchestration::codec::OpenclawLoaded::from_orchestration(
268 orchestration,
269 {
270 let mut v = load_home(source, HomeFlavor::Orchestrator.into())?.vault;
271 v.extend(vault);
272 v
273 },
274 );
275 let report = to_openclaw(&loaded, dest)?;
276 OrchestrationDecompiled {
277 written: report.written,
278 refused: report.refused.iter().map(RefusalRow::from).collect(),
279 notes: report.notes,
280 rows_byte: Some(report.rows_byte),
281 rows_emitted: Some(report.rows_emitted),
282 }
283 }
284 (OrchestrationHarness::Hermes, SourceFlavor::Native) => {
285 let mut loaded = from_hermes(source)?;
286 loaded.orchestration = orchestration;
287 loaded.vault.extend(vault);
288 let report = to_hermes(&loaded, dest, None)?;
289 OrchestrationDecompiled {
290 written: report.written,
291 refused: report.refused.iter().map(RefusalRow::from).collect(),
292 ..OrchestrationDecompiled::default()
293 }
294 }
295 (OrchestrationHarness::Openclaw, SourceFlavor::Native) => {
296 let mut loaded = from_openclaw(source)?;
297 loaded.orchestration = orchestration;
298 loaded.vault.extend(vault);
299 let report = to_openclaw(&loaded, dest)?;
300 OrchestrationDecompiled {
301 written: report.written,
302 refused: report.refused.iter().map(RefusalRow::from).collect(),
303 notes: report.notes,
304 rows_byte: Some(report.rows_byte),
305 rows_emitted: Some(report.rows_emitted),
306 }
307 }
308 })
309}
310
311pub fn import(
319 from: OrchestrationHarness,
320 home: &Path,
321 into: &Path,
322) -> Result<OrchestrationImported> {
323 let (orchestration, vault, sources): (
324 Orchestration,
325 BTreeMap<String, String>,
326 BTreeMap<String, PathBuf>,
327 ) = match from {
328 OrchestrationHarness::Hermes => {
329 let loaded = from_hermes(home)?;
330 let sources = loaded
331 .io
332 .iter()
333 .filter_map(|(name, io)| Some((name.clone(), io.source_dir.clone()?)))
334 .collect();
335 (loaded.orchestration, loaded.vault, sources)
336 }
337 OrchestrationHarness::Openclaw => {
338 let loaded = from_openclaw(home)?;
339 let sources = loaded
342 .profiles
343 .iter()
344 .map(|(name, io)| {
345 let src = if name == "default" {
346 loaded.root.state_dir.clone()
347 } else {
348 io.source_dir.clone()
349 };
350 (name.clone(), src)
351 })
352 .collect();
353 (loaded.orchestration, loaded.vault, sources)
354 }
355 };
356 let mut loaded = LoadedHome {
357 orchestration,
358 vault,
359 io: BTreeMap::new(),
360 };
361 repoint(&mut loaded.orchestration, into);
362 save_home(&mut loaded, Some(into))?;
363 let mut carried = Vec::new();
364 for (name, profile) in &loaded.orchestration.profiles {
365 let Some(src) = sources.get(name) else {
366 continue;
367 };
368 let files: Vec<String> = profile
373 .residue
374 .files
375 .iter()
376 .filter(|rel| !OWNED_FILES.contains(&rel.as_str()))
377 .cloned()
378 .collect();
379 for rel in carry_unmodeled(&files, src, &profile.dir)? {
380 carried.push(if name == "default" {
381 rel
382 } else {
383 format!("profiles/{name}/{rel}")
384 });
385 }
386 }
387 Ok(OrchestrationImported {
388 vault_keys: keys(&loaded.vault),
389 orchestration: loaded.orchestration,
390 root: into.to_path_buf(),
391 carried,
392 })
393}
394
395pub fn export(
402 to: OrchestrationHarness,
403 root: &Path,
404 dest: &Path,
405) -> Result<OrchestrationDecompiled> {
406 let loaded = load_home(root, Flavor::Orchestrator)?;
407 decompile(
408 to,
409 loaded.orchestration,
410 root,
411 SourceFlavor::Orchestrator,
412 dest,
413 loaded.vault,
414 )
415}