KeiSeiKit-1.0/_primitives/_rust/kei-import-project/src/plan_render.rs
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

135 lines
5.4 KiB
Rust

//! plan_render — markdown renderer for MigrationPlan.
//!
//! Constructor Pattern: one responsibility, ≤200 LOC, ≤30 LOC per fn.
use crate::plan_generator::{MigrationPlan, PhaseStatus};
/// Render a `MigrationPlan` to the HERMES-MIGRATION-PLAN.md format.
pub fn render_markdown(plan: &MigrationPlan) -> String {
let mut out = String::new();
push_header(&mut out, plan);
push_status_banner(&mut out, plan);
push_phase_table(&mut out, plan);
push_per_phase_detail(&mut out, plan);
push_unmatched(&mut out, plan);
push_follow_up(&mut out);
out
}
fn push_header(out: &mut String, plan: &MigrationPlan) {
out.push_str(&format!("# {} — Migration Plan\n\n", plan.project_name));
out.push_str(&format!("> Generated: {}\n", plan.generated_at));
out.push_str(&format!("> Source: {}\n", plan.source_repo));
out.push_str(&format!("> Average confidence: {:.2}\n\n", plan.total_confidence_avg));
}
fn push_status_banner(out: &mut String, plan: &MigrationPlan) {
out.push_str("## STATUS BANNER\n\n");
let blocked: usize = plan
.phases
.iter()
.filter(|p| p.initial_status == PhaseStatus::BlockedNeedsReview)
.count();
if plan.phases.is_empty() {
out.push_str("> **WARNING: no modules matched any trait at the given threshold.**\n");
out.push_str("> Lower `--threshold` or check that the repo contains Rust crates.\n\n");
} else if blocked > 0 {
out.push_str(&format!(
"> **AUTO-GENERATED plan. {blocked} phase(s) blocked — needs human triage.**\n"
));
out.push_str("> Run `kei-import-project execute <plan.md>` (Phase 5) after triage.\n\n");
} else {
out.push_str("> **AUTO-GENERATED initial plan. All phases initial status: scaffolding.**\n");
out.push_str(
"> Run `kei-import-project execute <plan.md>` (Phase 5) to spawn agents per phase.\n",
);
out.push_str("> Each agent must finish with STATUS-TRUTH MARKER (RULE 0.16).\n\n");
}
}
fn push_phase_table(out: &mut String, plan: &MigrationPlan) {
out.push_str("| Phase | Trait family | Modules | Priority | Initial status |\n");
out.push_str("|---|---|---:|---:|---|\n");
for p in &plan.phases {
let status = match p.initial_status {
PhaseStatus::Scaffolding => "scaffolding",
PhaseStatus::BlockedNeedsReview => "blocked-needs-review",
};
out.push_str(&format!(
"| {} | {} | {} | {} | {} |\n",
p.id,
p.trait_family,
p.modules.len(),
p.priority,
status
));
}
out.push('\n');
}
fn push_per_phase_detail(out: &mut String, plan: &MigrationPlan) {
out.push_str("## Per-phase detail\n\n");
for p in &plan.phases {
out.push_str(&format!("### {}{}\n\n", p.id, p.trait_family));
out.push_str("Modules to port:\n");
let mut sorted_mods = p.modules.clone();
sorted_mods.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
for (name, conf) in &sorted_mods {
out.push_str(&format!("- {} (confidence {:.2})\n", name, conf));
}
out.push_str("\nVerification gate (RULE 0.13 + RULE 0.16):\n");
out.push_str("- `cargo check --workspace` PASS\n");
out.push_str("- `cargo test -p <crate>` PASS\n");
out.push_str("- STATUS-TRUTH MARKER `shipped: functional`\n\n");
}
}
fn push_unmatched(out: &mut String, plan: &MigrationPlan) {
out.push_str("## Unmatched modules\n\n");
if plan.unmatched_modules.is_empty() {
out.push_str("All modules matched at the given threshold.\n\n");
} else {
out.push_str(&format!(
"These do not match any KeiSeiKit-runtime-core trait at threshold {:.2}.\n",
plan.total_confidence_avg
));
out.push_str("Manual classification required before they can be ported.\n\n");
for m in &plan.unmatched_modules {
out.push_str(&format!("- {}\n", m));
}
out.push('\n');
}
}
fn push_follow_up(out: &mut String) {
out.push_str("## Follow-up\n\n");
out.push_str(
"- Apply skeletons: `kei-import-project skeleton --module <m> --trait-name <t>`\n",
);
out.push_str("- Execute phases: `kei-import-project execute plan.md` (Phase 5)\n");
}
// ─────────────────────────── timestamp helper ──────────────────────────────
/// Convert Unix epoch seconds to an ISO-8601 UTC string.
/// Used by plan_generator (avoids chrono dep; pulled here to respect ≤200 LOC budget).
pub(crate) fn epoch_secs_to_iso(secs: u64) -> String {
let s = secs % 60;
let m = (secs / 60) % 60;
let h = (secs / 3600) % 24;
let mut days = secs / 86400;
let mut year = 1970u64;
loop {
let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
let dy = if leap { 366 } else { 365 };
if days < dy { break; }
days -= dy;
year += 1;
}
let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
let months = if leap { [31u64,29,31,30,31,30,31,31,30,31,30,31] }
else { [31,28,31,30,31,30,31,31,30,31,30,31] };
let mut month = 1u64;
for &dm in &months { if days < dm { break; } days -= dm; month += 1; }
format!("{year:04}-{month:02}-{:02}T{h:02}:{m:02}:{s:02}Z", days + 1)
}