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
159pub fn load(root: &Path, flavor: HomeFlavor) -> Result<OrchestrationRead> {
161 let loaded = load_home(root, flavor.into())?;
162 Ok(OrchestrationRead {
163 vault_keys: keys(&loaded.vault),
164 orchestration: loaded.orchestration,
165 })
166}
167
168pub fn save(
175 root: &Path,
176 orchestration: Orchestration,
177 vault: BTreeMap<String, String>,
178) -> Result<OrchestrationSaved> {
179 let mut loaded = if root.is_dir() {
180 load_home(root, Flavor::Orchestrator)?
181 } else {
182 LoadedHome {
183 orchestration: orchestration.clone(),
184 vault: BTreeMap::new(),
185 io: BTreeMap::new(),
186 }
187 };
188 loaded.orchestration = orchestration;
189 repoint(&mut loaded.orchestration, root);
190 loaded.vault.extend(vault);
191 save_home(&mut loaded, Some(root))?;
192 Ok(OrchestrationSaved {
193 written: true,
194 root: root.to_path_buf(),
195 })
196}
197
198pub fn compile(from: OrchestrationHarness, home: &Path) -> Result<OrchestrationRead> {
200 Ok(match from {
201 OrchestrationHarness::Hermes => {
202 let loaded = from_hermes(home)?;
203 OrchestrationRead {
204 vault_keys: keys(&loaded.vault),
205 orchestration: loaded.orchestration,
206 }
207 }
208 OrchestrationHarness::Openclaw => {
209 let loaded = from_openclaw(home)?;
210 OrchestrationRead {
211 vault_keys: keys(&loaded.vault),
212 orchestration: loaded.orchestration,
213 }
214 }
215 })
216}
217
218pub fn decompile(
225 to: OrchestrationHarness,
226 orchestration: Orchestration,
227 source: &Path,
228 source_flavor: SourceFlavor,
229 dest: &Path,
230 vault: BTreeMap<String, String>,
231) -> Result<OrchestrationDecompiled> {
232 Ok(match (to, source_flavor) {
233 (OrchestrationHarness::Hermes, SourceFlavor::Orchestrator) => {
237 let mut loaded = load_home(source, HomeFlavor::Orchestrator.into())?;
238 loaded.orchestration = orchestration;
239 loaded.vault.extend(vault);
240 let report = to_hermes(&loaded, dest, None)?;
241 OrchestrationDecompiled {
242 written: report.written,
243 refused: report.refused.iter().map(RefusalRow::from).collect(),
244 ..OrchestrationDecompiled::default()
245 }
246 }
247 (OrchestrationHarness::Openclaw, SourceFlavor::Orchestrator) => {
248 let loaded =
249 supercode_interchange::orchestration::codec::OpenclawLoaded::from_orchestration(
250 orchestration,
251 {
252 let mut v = load_home(source, HomeFlavor::Orchestrator.into())?.vault;
253 v.extend(vault);
254 v
255 },
256 );
257 let report = to_openclaw(&loaded, dest)?;
258 OrchestrationDecompiled {
259 written: report.written,
260 refused: report.refused.iter().map(RefusalRow::from).collect(),
261 notes: report.notes,
262 rows_byte: Some(report.rows_byte),
263 rows_emitted: Some(report.rows_emitted),
264 }
265 }
266 (OrchestrationHarness::Hermes, SourceFlavor::Native) => {
267 let mut loaded = from_hermes(source)?;
268 loaded.orchestration = orchestration;
269 loaded.vault.extend(vault);
270 let report = to_hermes(&loaded, dest, None)?;
271 OrchestrationDecompiled {
272 written: report.written,
273 refused: report.refused.iter().map(RefusalRow::from).collect(),
274 ..OrchestrationDecompiled::default()
275 }
276 }
277 (OrchestrationHarness::Openclaw, SourceFlavor::Native) => {
278 let mut loaded = from_openclaw(source)?;
279 loaded.orchestration = orchestration;
280 loaded.vault.extend(vault);
281 let report = to_openclaw(&loaded, dest)?;
282 OrchestrationDecompiled {
283 written: report.written,
284 refused: report.refused.iter().map(RefusalRow::from).collect(),
285 notes: report.notes,
286 rows_byte: Some(report.rows_byte),
287 rows_emitted: Some(report.rows_emitted),
288 }
289 }
290 })
291}
292
293pub fn import(
301 from: OrchestrationHarness,
302 home: &Path,
303 into: &Path,
304) -> Result<OrchestrationImported> {
305 let (orchestration, vault, sources): (
306 Orchestration,
307 BTreeMap<String, String>,
308 BTreeMap<String, PathBuf>,
309 ) = match from {
310 OrchestrationHarness::Hermes => {
311 let loaded = from_hermes(home)?;
312 let sources = loaded
313 .io
314 .iter()
315 .filter_map(|(name, io)| Some((name.clone(), io.source_dir.clone()?)))
316 .collect();
317 (loaded.orchestration, loaded.vault, sources)
318 }
319 OrchestrationHarness::Openclaw => {
320 let loaded = from_openclaw(home)?;
321 let sources = loaded
324 .profiles
325 .iter()
326 .map(|(name, io)| {
327 let src = if name == "default" {
328 loaded.root.state_dir.clone()
329 } else {
330 io.source_dir.clone()
331 };
332 (name.clone(), src)
333 })
334 .collect();
335 (loaded.orchestration, loaded.vault, sources)
336 }
337 };
338 let mut loaded = LoadedHome {
339 orchestration,
340 vault,
341 io: BTreeMap::new(),
342 };
343 repoint(&mut loaded.orchestration, into);
344 save_home(&mut loaded, Some(into))?;
345 let mut carried = Vec::new();
346 for (name, profile) in &loaded.orchestration.profiles {
347 let Some(src) = sources.get(name) else {
348 continue;
349 };
350 let files: Vec<String> = profile
355 .residue
356 .files
357 .iter()
358 .filter(|rel| !OWNED_FILES.contains(&rel.as_str()))
359 .cloned()
360 .collect();
361 for rel in carry_unmodeled(&files, src, &profile.dir)? {
362 carried.push(if name == "default" {
363 rel
364 } else {
365 format!("profiles/{name}/{rel}")
366 });
367 }
368 }
369 Ok(OrchestrationImported {
370 vault_keys: keys(&loaded.vault),
371 orchestration: loaded.orchestration,
372 root: into.to_path_buf(),
373 carried,
374 })
375}
376
377pub fn export(
384 to: OrchestrationHarness,
385 root: &Path,
386 dest: &Path,
387) -> Result<OrchestrationDecompiled> {
388 let loaded = load_home(root, Flavor::Orchestrator)?;
389 decompile(
390 to,
391 loaded.orchestration,
392 root,
393 SourceFlavor::Orchestrator,
394 dest,
395 loaded.vault,
396 )
397}