use super::*;
const LIFECYCLE_FILES: &[&str] = &[
"lib.rs", "main.rs", "build.rs", "build_assets.rs", "application.rs", "config.rs", "error.rs", "domain/mod.rs",
"domain/types/mod.rs", "domain/types/error.rs", "domain/services/mod.rs",
"persistence/mod.rs", "persistence/compliance_schema.rs", "bootstrapper.rs",
"registrations.rs", "server.rs", "worker.rs", "signals.rs", "tests/smoke.rs",
"__test__/http.rs", "Makefile", ".env.example", "domain/Cargo.toml",
];
pub(super) fn replaces(relative: &str) -> bool { LIFECYCLE_FILES.contains(&relative) }
pub(super) fn preflight(root: &Path, name: &str, kind: RustProjectKind) -> Result<()> {
let manifest = read_manifest_value(&root.join(".forklaunch/manifest.toml"))?;
let project = project_table(&manifest, name).context("Conversion project missing")?;
let directory = root.join(crate_path_for_project(&manifest, name).context("Conversion crate path missing")?);
anyhow::ensure!(directory.join("lib.rs").is_file(), "Legacy Rust project conversion requires an explicit lifecycle migration before changing kind/backend");
let description = cargo_description(&directory.join("Cargo.toml"))?.unwrap_or_default();
let templates = match kind {
RustProjectKind::Service => {
let templates = native_layout::service_templates(&directory, name, &description);
if project.get("metadata").and_then(|m| m.get("persistence")).and_then(Value::as_str) == Some("postgresql") {
service_database::templates(&directory, name, templates)?
} else { templates }
}
RustProjectKind::Worker if directory.join("worker_hooks.rs").exists() || directory.join("consumer/worker_hooks.rs").exists() => {
bail!("Apalis runtime changes require an explicit queue/lifecycle migration; existing application hooks are preserved");
}
RustProjectKind::Worker => {
anyhow::ensure!(project.get("metadata").and_then(|m| m.get("workerService")).and_then(Value::as_bool) != Some(true),
"Worker producer changes require an explicit queue/process migration; existing HTTP contracts are preserved");
let templates = if let Some(backend) = worker::backend(root, name)? {
worker::templates(root, &directory, name, &description, backend)?
} else { rust_crate_templates(&directory, name, &description, kind) };
let templates = native_layout::flatten(&directory, name, kind, templates);
if contracts_layout::directory(&directory).is_some() {
worker::consumer_contracts(&directory, name, templates)?
} else { templates }
}
RustProjectKind::Library => native_layout::flatten(&directory, name, kind, rust_crate_templates(&directory, name, &description, kind)),
};
let templates = native_mappers::templates(&directory, directory.join("domain/mappers/health.rs").is_file(), templates)?;
let templates = native_layout::framework_paths(root, templates);
let cargo_template = templates.iter().find(|template| template.path == directory.join("Cargo.toml"))
.context("Conversion Cargo template missing")?;
let mut expected_cargo: Value = toml::from_str(&dependencies::render_for_path(&cargo_template.path, &cargo_template.content)?)?;
let mut actual_cargo: Value = toml::from_str(&read_to_string(&cargo_template.path)?)?;
for cargo in [&mut expected_cargo, &mut actual_cargo] {
if let Some(package) = cargo.get_mut("package").and_then(Value::as_table_mut) { package.remove("metadata"); }
}
if let Some(dependencies) = actual_cargo.get_mut("dependencies").and_then(Value::as_table_mut) {
dependencies.retain(|name, _| expected_cargo.get("dependencies").and_then(Value::as_table).is_some_and(|known| known.contains_key(name)));
}
anyhow::ensure!(actual_cargo == expected_cargo,
"Conversion would overwrite customized lifecycle manifest {}; migrate it explicitly before changing kind/backend", cargo_template.path.display());
for relative in LIFECYCLE_FILES {
let path = directory.join(relative);
anyhow::ensure!(!path.exists() || templates.iter().any(|template| template.path == path),
"Conversion would overwrite unrecognized lifecycle file {}; migrate it explicitly before changing kind/backend", path.display());
}
for template in templates {
let Ok(relative) = template.path.strip_prefix(&directory) else { continue; };
let relative = relative.to_string_lossy();
if !replaces(&relative) || !template.path.exists() { continue; }
anyhow::ensure!(!template.path.is_symlink(), "Conversion would replace symlinked lifecycle file {}; migrate it explicitly", template.path.display());
let expected = dependencies::render_for_path(&template.path, &template.content)?;
let marked = crate::core::watermark::apply_watermark(&RenderedTemplate { content: expected.clone(), ..template.clone() })?;
let actual = read_to_string(&template.path)?;
let normalized = native_layout::conversion_source(&template.path, &actual);
anyhow::ensure!(
[expected, marked].iter().any(|source| native_layout::conversion_source(&template.path, source) == normalized),
"Conversion would overwrite customized lifecycle file {}; migrate it explicitly before changing kind/backend", template.path.display()
);
}
Ok(())
}