use std::fs;
use std::path::Path;
use spec_spine_core::shard::{self, BY_SPEC_DIR};
use spec_spine_core::{
CompileOutcome, Freshness, compare_committed_registry, registry_dir, registry_shard_files,
};
use spec_spine_types::{BUILD_META_SCHEMA_VERSION, BuildMeta, Error, Severity};
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use crate::load_repo_config;
pub fn run(repo: &Path, check: bool) -> Result<u8, Error> {
let cfg = load_repo_config(repo)?;
let outcome = spec_spine_core::compile(&cfg, repo)?;
if check {
if !outcome.validation_passed {
report_validation_failure(&outcome);
return Ok(1);
}
return match compare_committed_registry(&cfg, repo, &outcome.shards)? {
Freshness::Fresh => {
outln!(
"spec-registry is fresh: {} shard(s) match the corpus",
outcome.registry.specs.len()
);
Ok(0)
}
Freshness::Stale { actual, .. } => {
eprintln!("{actual}");
eprintln!("spec-registry is STALE: run `spec-spine compile` and commit the result");
Ok(2)
}
};
}
let out_dir = registry_dir(&cfg, repo);
fs::create_dir_all(&out_dir)
.map_err(|e| Error::Io(format!("create {}: {e}", out_dir.display())))?;
let shard_files = registry_shard_files(&outcome.shards)?;
let by_spec = out_dir.join(BY_SPEC_DIR);
shard::sync_dir(&by_spec, &shard_files)?;
let legacy = out_dir.join("registry.json");
if legacy.exists() {
fs::remove_file(&legacy)
.map_err(|e| Error::Io(format!("remove {}: {e}", legacy.display())))?;
}
let meta = BuildMeta {
schema_version: BUILD_META_SCHEMA_VERSION.to_string(),
built_at: now_rfc3339(),
compiler_id: cfg.branding.compiler_id.clone(),
compiler_version: env!("CARGO_PKG_VERSION").to_string(),
};
let meta_json =
serde_json::to_string_pretty(&meta).map_err(|e| Error::Schema(e.to_string()))? + "\n";
let meta_path = out_dir.join("build-meta.json");
fs::write(&meta_path, meta_json)
.map_err(|e| Error::Io(format!("write {}: {e}", meta_path.display())))?;
let warnings = outcome
.registry
.validation
.violations
.iter()
.filter(|v| v.severity == Severity::Warning)
.count();
if outcome.validation_passed {
outln!(
"compiled {} spec(s) -> {} ({} warning(s))",
outcome.registry.specs.len(),
by_spec.display(),
warnings
);
Ok(0)
} else {
report_validation_failure(&outcome);
Ok(1)
}
}
fn report_validation_failure(outcome: &CompileOutcome) {
let violations = &outcome.registry.validation.violations;
for v in violations {
if v.severity == Severity::Error {
let at = v.path.as_deref().unwrap_or("-");
eprintln!(" {} [{}] {}", v.code, at, v.message);
}
}
let errors = violations
.iter()
.filter(|v| v.severity == Severity::Error)
.count();
let warnings = violations
.iter()
.filter(|v| v.severity == Severity::Warning)
.count();
eprintln!(
"validation FAILED: {errors} error(s), {warnings} warning(s) across {} spec(s)",
outcome.registry.specs.len()
);
}
fn now_rfc3339() -> String {
OffsetDateTime::now_utc()
.format(&Rfc3339)
.unwrap_or_else(|_| "unknown".to_string())
}