feat: add ecc2 legacy migration scaffold

This commit is contained in:
Affaan Mustafa
2026-04-10 10:57:13 -07:00
parent d36e9c48a4
commit 046af44065

View File

@@ -596,6 +596,18 @@ enum MigrationCommands {
#[arg(long)]
json: bool,
},
/// Scaffold migration artifacts on disk from a legacy workspace audit
Scaffold {
/// Path to the legacy Hermes/OpenClaw workspace root
#[arg(long)]
source: PathBuf,
/// Directory where scaffolded migration artifacts should be written
#[arg(long)]
output_dir: PathBuf,
/// Emit machine-readable JSON instead of the human summary
#[arg(long)]
json: bool,
},
}
#[derive(clap::Subcommand, Debug)]
@@ -946,6 +958,14 @@ struct LegacyMigrationPlanReport {
steps: Vec<LegacyMigrationPlanStep>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct LegacyMigrationScaffoldReport {
source: String,
output_dir: String,
files_written: Vec<String>,
steps_scaffolded: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
struct RemoteDispatchHttpRequest {
task: String,
@@ -1701,6 +1721,20 @@ async fn main() -> Result<()> {
println!("{rendered}");
}
}
MigrationCommands::Scaffold {
source,
output_dir,
json,
} => {
let audit = build_legacy_migration_audit_report(&source)?;
let plan = build_legacy_migration_plan_report(&audit);
let report = write_legacy_migration_scaffold(&plan, &output_dir)?;
if json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
println!("{}", format_legacy_migration_scaffold_human(&report));
}
}
},
Some(Commands::Graph { command }) => match command {
GraphCommands::AddEntity {
@@ -4987,6 +5021,62 @@ fn build_legacy_migration_plan_report(
}
}
fn write_legacy_migration_scaffold(
plan: &LegacyMigrationPlanReport,
output_dir: &Path,
) -> Result<LegacyMigrationScaffoldReport> {
fs::create_dir_all(output_dir).with_context(|| {
format!(
"create migration scaffold output directory: {}",
output_dir.display()
)
})?;
let plan_path = output_dir.join("migration-plan.md");
let config_path = output_dir.join("ecc2.migration.toml");
fs::write(&plan_path, format_legacy_migration_plan_human(plan))
.with_context(|| format!("write migration plan: {}", plan_path.display()))?;
fs::write(&config_path, render_legacy_migration_config_scaffold(plan))
.with_context(|| format!("write migration config scaffold: {}", config_path.display()))?;
Ok(LegacyMigrationScaffoldReport {
source: plan.source.clone(),
output_dir: output_dir.display().to_string(),
files_written: vec![
plan_path.display().to_string(),
config_path.display().to_string(),
],
steps_scaffolded: plan.steps.len(),
})
}
fn render_legacy_migration_config_scaffold(plan: &LegacyMigrationPlanReport) -> String {
let mut sections = vec![
format!(
"# ECC2 migration scaffold generated from {}\n# Review every section before merging it into a real ecc2.toml.",
plan.source
),
];
for step in &plan.steps {
if step.config_snippets.is_empty() {
continue;
}
sections.push(format!(
"\n# {} [{} -> {}]",
step.title,
format_legacy_migration_readiness(step.readiness),
step.target_surface
));
for snippet in &step.config_snippets {
sections.push(snippet.clone());
}
}
sections.join("\n\n")
}
fn format_legacy_migration_audit_human(report: &LegacyMigrationAuditReport) -> String {
let mut lines = vec![
format!("Legacy migration audit: {}", report.source),
@@ -5092,6 +5182,19 @@ fn format_legacy_migration_plan_human(report: &LegacyMigrationPlanReport) -> Str
lines.join("\n")
}
fn format_legacy_migration_scaffold_human(report: &LegacyMigrationScaffoldReport) -> String {
let mut lines = vec![
format!("Legacy migration scaffold written for {}", report.source),
format!("- output dir {}", report.output_dir),
format!("- steps scaffolded {}", report.steps_scaffolded),
"- files".to_string(),
];
for path in &report.files_written {
lines.push(format!(" {}", path));
}
lines.join("\n")
}
fn format_graph_recall_human(
entries: &[session::ContextGraphRecallEntry],
session_id: Option<&str>,
@@ -7568,6 +7671,37 @@ mod tests {
}
}
#[test]
fn cli_parses_migrate_scaffold_command() {
let cli = Cli::try_parse_from([
"ecc",
"migrate",
"scaffold",
"--source",
"/tmp/hermes",
"--output-dir",
"/tmp/migration-scaffold",
"--json",
])
.expect("migrate scaffold should parse");
match cli.command {
Some(Commands::Migrate {
command:
MigrationCommands::Scaffold {
source,
output_dir,
json,
},
}) => {
assert_eq!(source, PathBuf::from("/tmp/hermes"));
assert_eq!(output_dir, PathBuf::from("/tmp/migration-scaffold"));
assert!(json);
}
_ => panic!("expected migrate scaffold subcommand"),
}
}
#[test]
fn legacy_migration_audit_report_maps_detected_artifacts() -> Result<()> {
let tempdir = TestDir::new("legacy-migration-audit")?;
@@ -7667,6 +7801,33 @@ mod tests {
Ok(())
}
#[test]
fn legacy_migration_scaffold_writes_plan_and_config_files() -> Result<()> {
let tempdir = TestDir::new("legacy-migration-scaffold")?;
let root = tempdir.path();
fs::create_dir_all(root.join("workspace/notes"))?;
fs::create_dir_all(root.join("skills/ecc-imports"))?;
fs::write(root.join("config.yaml"), "model: claude\n")?;
fs::write(root.join("workspace/notes/recovery.md"), "# recovery\n")?;
fs::write(root.join("skills/ecc-imports/triage.md"), "# triage\n")?;
let audit = build_legacy_migration_audit_report(root)?;
let plan = build_legacy_migration_plan_report(&audit);
let output_dir = root.join("out");
let report = write_legacy_migration_scaffold(&plan, &output_dir)?;
assert_eq!(report.steps_scaffolded, plan.steps.len());
assert_eq!(report.files_written.len(), 2);
let plan_text = fs::read_to_string(output_dir.join("migration-plan.md"))?;
let config_text = fs::read_to_string(output_dir.join("ecc2.migration.toml"))?;
assert!(plan_text.contains("Legacy migration plan"));
assert!(config_text.contains("[memory_connectors.hermes_workspace]"));
assert!(config_text.contains("[orchestration_templates.legacy_workflow]"));
Ok(())
}
#[test]
fn format_decisions_human_renders_details() {
let text = format_decisions_human(