use crate::date::{Date, DateTime};
use crate::document::Document;
use crate::frontmatter::Frontmatter;
use crate::trust::Status;
use crate::yaml::{Mapping, Value};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BundleInitOptions {
pub title: String,
pub create_sample: bool,
pub sample_name: String,
pub author: Option<String>,
pub force: bool,
}
impl Default for BundleInitOptions {
fn default() -> Self {
Self {
title: "OKF Bundle".to_string(),
create_sample: true,
sample_name: "overview".to_string(),
author: None,
force: false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConceptOptions {
pub type_: String,
pub title: Option<String>,
pub description: Option<String>,
pub status: Status,
pub author: Option<String>,
pub attested: bool,
pub tags: Vec<String>,
pub force: bool,
}
impl Default for ConceptOptions {
fn default() -> Self {
Self {
type_: "Concept".to_string(),
title: None,
description: None,
status: Status::Draft,
author: None,
attested: false,
tags: Vec::new(),
force: false,
}
}
}
#[must_use]
pub fn default_author() -> String {
if let Ok(author) = std::env::var("OKF_AUTHOR")
&& !author.trim().is_empty()
{
return author.trim().to_string();
}
for env_key in &["USER", "USERNAME", "LOGNAME"] {
if let Ok(user) = std::env::var(env_key) {
let clean: String = user
.chars()
.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-')
.collect();
if !clean.is_empty() {
return format!("human:{clean}");
}
}
}
"human:author".to_string()
}
#[must_use]
pub fn title_from_name(name: &str) -> String {
let name = name.trim_end_matches(".md");
let mut words = Vec::new();
for part in name.split(['_', '-']) {
if part.is_empty() {
continue;
}
let mut chars = part.chars();
let first = chars.next().unwrap_or_default().to_uppercase().to_string();
words.push(format!("{first}{}", chars.as_str()));
}
if words.is_empty() {
"Untitled".to_string()
} else {
words.join(" ")
}
}
#[must_use]
pub fn current_iso_timestamp() -> String {
DateTime::now_utc().map_or_else(
|| "2026-01-01T00:00:00Z".to_string(),
|dt| {
let clean = DateTime {
date: dt.date,
hour: dt.hour,
minute: dt.minute,
second: dt.second,
nanosecond: 0,
offset_minutes: Some(0),
has_time: true,
};
clean.to_string()
},
)
}
#[must_use]
pub fn build_concept_document(options: &ConceptOptions, title: &str) -> Document {
let mut fm = Frontmatter::new();
let is_attested =
options.attested || options.type_ == crate::computation::ATTESTED_COMPUTATION_TYPE;
let actual_type = if is_attested {
crate::computation::ATTESTED_COMPUTATION_TYPE
} else {
options.type_.as_str()
};
fm.set("type", Value::String(actual_type.to_string()));
fm.set("title", Value::String(title.to_string()));
let desc = options
.description
.as_deref()
.filter(|d| !d.trim().is_empty())
.map_or_else(
|| format!("Overview and details for {title}."),
ToString::to_string,
);
fm.set("description", Value::String(desc));
fm.set("status", Value::String(options.status.to_string()));
let author = options.author.clone().unwrap_or_else(default_author);
let mut gen_map = Mapping::new();
gen_map.insert("by", Value::String(author));
gen_map.insert("at", Value::String(current_iso_timestamp()));
fm.set("generated", Value::Mapping(gen_map));
if !options.tags.is_empty() {
let tag_values: Vec<Value> = options
.tags
.iter()
.map(|t| Value::String(t.clone()))
.collect();
fm.set("tags", Value::Sequence(tag_values));
}
if is_attested {
fm.set("runtime", Value::String("python".to_string()));
let mut param_map = Mapping::new();
param_map.insert("name", Value::String("input_data".to_string()));
param_map.insert("type", Value::String("string".to_string()));
param_map.insert("required", Value::Bool(true));
fm.set(
"parameters",
Value::Sequence(vec![Value::Mapping(param_map)]),
);
let mut exec_map = Mapping::new();
exec_map.insert(
"resource",
Value::String("references/skills/run.md".to_string()),
);
exec_map.insert(
"receipt",
Value::Sequence(vec![Value::String("result".to_string())]),
);
fm.set("executor", Value::Mapping(exec_map));
let mut att_map = Mapping::new();
att_map.insert(
"resource",
Value::String("references/attesters/verify.py".to_string()),
);
fm.set("attester", Value::Mapping(att_map));
let body = format!(
"# {title}\n\n# Computation\n\n```python\n# Sanctioned computation logic\n```\n"
);
Document::new(fm, body)
} else {
let body = format!("# {title}\n\nDescribe {title} here.\n");
Document::new(fm, body)
}
}
pub fn create_concept(path: impl AsRef<Path>, options: &ConceptOptions) -> io::Result<PathBuf> {
let mut path = path.as_ref().to_path_buf();
if path.extension().is_none() {
path.set_extension("md");
}
for comp in path.components() {
match comp {
std::path::Component::Normal(seg) => {
if let Some(s) = seg.to_str() {
let s_no_ext = s.strip_suffix(".md").unwrap_or(s);
if let Err(e) = crate::concept_id::validate_segment(s_no_ext) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, e.to_string()));
}
}
}
std::path::Component::ParentDir => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"concept path cannot contain `..`",
));
}
std::path::Component::CurDir
| std::path::Component::Prefix(_)
| std::path::Component::RootDir => {}
}
}
if path.exists() && !options.force {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("concept file already exists: {}", path.display()),
));
}
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)?;
}
let file_stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("concept");
let title = options
.title
.clone()
.unwrap_or_else(|| title_from_name(file_stem));
let doc = build_concept_document(options, &title);
fs::write(&path, doc.serialize())?;
Ok(path)
}
pub fn init_bundle(
root: impl AsRef<Path>,
options: &BundleInitOptions,
) -> io::Result<Vec<PathBuf>> {
let root = root.as_ref();
fs::create_dir_all(root)?;
let index_path = root.join("index.md");
let log_path = root.join("log.md");
if !options.force {
if index_path.exists() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("index.md already exists: {}", index_path.display()),
));
}
if log_path.exists() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("log.md already exists: {}", log_path.display()),
));
}
}
let mut created = Vec::new();
let sample_rel = if options.create_sample {
let sample_stem = if options.sample_name.trim().is_empty() {
"overview"
} else {
options.sample_name.trim().trim_end_matches(".md")
};
let sample_path = root.join(format!("{sample_stem}.md"));
let title = title_from_name(sample_stem);
let desc = format!("Initial {} concept for this bundle.", title.to_lowercase());
let concept_opts = ConceptOptions {
type_: "Concept".to_string(),
title: Some(title.clone()),
description: Some(desc.clone()),
status: Status::Draft,
author: options.author.clone(),
attested: false,
tags: Vec::new(),
force: options.force,
};
create_concept(&sample_path, &concept_opts)?;
created.push(sample_path);
Some((title, format!("{sample_stem}.md"), desc))
} else {
None
};
let index_text = if let Some((title, link, desc)) = sample_rel {
format!(
"---\nokf_version: \"{}\"\n---\n\n# Concept\n\n* [{title}]({link}) - {desc}\n",
crate::OKF_VERSION
)
} else {
format!(
"---\nokf_version: \"{}\"\n---\n\n# {}\n",
crate::OKF_VERSION,
options.title
)
};
fs::write(&index_path, index_text)?;
created.push(index_path);
let today = Date::today_utc().unwrap_or(Date {
year: 2026,
month: 1,
day: 1,
});
let log_text = format!("# Update Log\n\n## {today}\n* **Creation**: Initialized OKF bundle.\n");
fs::write(&log_path, log_text)?;
created.push(log_path);
Ok(created)
}