use anyhow::{Context, Result};
use clap::Args;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::cli::CmdCtx;
#[derive(Args, Debug)]
pub struct InitArgs {
pub name: String,
#[arg(long)]
pub domain: String,
#[arg(long)]
pub out: Option<PathBuf>,
#[arg(long)]
pub extras: bool,
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub force: bool,
#[arg(skip)]
pub stubs: bool,
}
pub async fn run(args: InitArgs, ctx: &CmdCtx) -> Result<()> {
let slug = slugify(&args.name);
let out = args.out.unwrap_or_else(|| PathBuf::from(&slug));
if out.exists() && !args.force {
anyhow::bail!(
"directory '{}' already exists; use --force to overwrite",
out.display()
);
}
let today = today_iso8601();
let name = &args.name;
let domain = &args.domain;
let title = args
.title
.as_deref()
.unwrap_or("TODO: human-readable display name");
write_file(
&out.join("manifest.json"),
&manifest_json(name, domain, title, &today),
)?;
let machine = out.join("machine");
if args.stubs {
write_file(
&machine.join("system_prompt.md"),
&system_prompt_md(name, domain),
)?;
write_file(&machine.join("glossary.json"), GLOSSARY_JSON)?;
write_file(&machine.join("rules.json"), RULES_JSON)?;
write_file(&machine.join("ontology.json"), ONTOLOGY_JSON)?;
write_file(&machine.join("constraints.json"), CONSTRAINTS_JSON)?;
write_file(&machine.join("decision_trees.json"), DECISION_TREES_JSON)?;
write_file(
&machine.join("retrieval_chunks.jsonl"),
RETRIEVAL_CHUNKS_JSONL,
)?;
} else {
std::fs::create_dir_all(&machine)
.with_context(|| format!("creating directory '{}'", machine.display()))?;
}
let evidence = out.join("evidence");
write_file(&evidence.join("sources.csv"), SOURCES_CSV)?;
write_file(&evidence.join("rights_log.csv"), RIGHTS_LOG_CSV)?;
write_file(&evidence.join("review_notes.md"), REVIEW_NOTES_MD)?;
let okf = out.join("okf");
write_file(&okf.join("index.md"), &okf_index_md(name, &today))?;
for subdir in &[
"terms",
"rules",
"constraints",
"procedures",
"chunks",
"ontology",
] {
write_file(&okf.join(subdir).join(".gitkeep"), "")?;
}
write_file(&out.join("README.md"), &readme_md(name, domain))?;
if args.extras {
write_file(&machine.join("eval_set.jsonl"), "")?;
write_file(&machine.join("knowledge_graph.json"), KNOWLEDGE_GRAPH_JSON)?;
if args.stubs {
write_file(
&out.join("human").join("handbook.md"),
&handbook_md(name, domain),
)?;
} else {
std::fs::create_dir_all(out.join("human"))
.with_context(|| "creating human/ directory".to_string())?;
}
write_file(&out.join("CHANGELOG.md"), &changelog_md(&today))?;
}
if !ctx.quiet {
println!("Initialized DKP pack at '{}'", out.display());
println!();
println!("Next steps:");
println!(" 1. Fill in the placeholder content in machine/ and evidence/");
println!(" 2. Run: dkp validate {}", out.display());
}
Ok(())
}
fn write_file(path: &Path, content: &str) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating directory '{}'", parent.display()))?;
}
std::fs::write(path, content).with_context(|| format!("writing '{}'", path.display()))?;
Ok(())
}
fn slugify(name: &str) -> String {
name.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-")
}
fn today_iso8601() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let days = secs / 86400;
let z = days as i64 + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{y:04}-{m:02}-{d:02}")
}
fn manifest_json(name: &str, domain: &str, title: &str, today: &str) -> String {
format!(
r#"{{
"spec": "1.0.0",
"name": "{name}",
"title": "{title}",
"version": "0.1.0",
"domain": "{domain}",
"audience": "TODO: describe the target user or agent type",
"intended_use": "TODO: describe authorized use cases (e.g. LLM context injection, RAG retrieval)",
"known_limitations": "TODO: describe scope gaps and what this pack does not cover",
"update_date": "{today}",
"compatibility": [],
"audience_profiles": []
}}
"#
)
}
fn system_prompt_md(name: &str, domain: &str) -> String {
format!(
"# {name}\n\
\n\
You are a knowledgeable assistant specialized in the **{domain}** domain.\n\
This Domain Knowledge Pack provides authoritative guidance on {domain} topics.\n\
\n\
## Domain Scope\n\
\n\
TODO: describe what topics this pack covers.\n\
\n\
## Behavioral Guidelines\n\
\n\
- Apply the domain rules and constraints provided in this pack.\n\
- Cite glossary terms when using domain-specific language.\n\
- Refer to retrieval chunks for specific facts and evidence.\n\
- Acknowledge the boundaries of this pack's scope when asked out-of-scope questions.\n\
\n\
## Known Limitations\n\
\n\
TODO: list what this pack does not cover.\n"
)
}
const GLOSSARY_JSON: &str = r#"{
"terms": [
{
"id": "term-001",
"term": "Example Term",
"definition": "Replace with your first domain-specific term and its authoritative definition.",
"stability": "stable",
"source_ref": "src-001"
}
]
}
"#;
const RULES_JSON: &str = r#"{
"rules": [
{
"id": "rule-001",
"title": "Example Affirmative Rule",
"description": "Replace with a rule describing what agents SHOULD or MUST do in this domain.",
"polarity": "affirmative",
"stability": "stable",
"source_ref": "src-001"
},
{
"id": "rule-002",
"title": "Example Prohibitive Rule",
"description": "Replace with a rule describing what agents MUST NOT do in this domain.",
"polarity": "prohibitive",
"stability": "stable",
"source_ref": "src-001"
}
]
}
"#;
const ONTOLOGY_JSON: &str = r#"{
"entity_types": [
{
"id": "example-entity",
"name": "Example Entity",
"description": "Replace with a domain entity type and its relevant attributes.",
"attributes": ["attribute_one", "attribute_two"]
}
]
}
"#;
const CONSTRAINTS_JSON: &str = r#"{
"edge_cases": [
{
"id": "edge-001",
"title": "Example Edge Case",
"description": "Replace with a situation where standard rules require special handling."
}
],
"anti_patterns": [
{
"id": "anti-001",
"title": "Example Anti-Pattern",
"description": "Replace with a common mistake or misconception to avoid in this domain."
}
],
"hard_limits": [
{
"id": "hard-001",
"title": "Example Hard Limit",
"description": "Replace with an absolute boundary that must never be crossed in this domain."
}
]
}
"#;
const DECISION_TREES_JSON: &str = r#"{
"trees": [
{
"id": "tree-001",
"title": "Example Decision Procedure",
"description": "Replace with a traversable decision procedure for a common domain question.",
"root": {
"question": "Replace with the first branching question.",
"branches": [
{
"condition": "Yes",
"next": { "answer": "Replace with the recommendation when the condition is true." }
},
{
"condition": "No",
"next": { "answer": "Replace with the recommendation when the condition is false." }
}
]
}
}
]
}
"#;
const RETRIEVAL_CHUNKS_JSONL: &str = "{\"id\":\"chunk-001\",\"title\":\"Example Retrieval Chunk\",\"chunk_text\":\"Replace with a self-contained, standalone piece of domain knowledge. Each chunk should be independently useful when retrieved by a RAG pipeline without additional context.\",\"tags\":[\"example\",\"placeholder\"],\"source_ref\":\"src-001\",\"confidence\":0.9,\"stability\":\"stable\"}\n";
const SOURCES_CSV: &str = "id,title,url,retrieved_date,license,notes\n\
src-001,Example Source,https://example.com,2024-01-01,TODO,Replace with your first authoritative source\n";
const RIGHTS_LOG_CSV: &str = "id,source_id,rights_holder,license_type,granted_date,expiry_date,notes\n\
rr-001,src-001,TODO Rights Holder,TODO License,2024-01-01,perpetual,Replace with actual rights information\n";
const REVIEW_NOTES_MD: &str = "# Review Notes\n\
\n\
TODO: Document editorial decisions, source quality assessments, and content review observations here.\n\
\n\
When the pack has passed editorial review (Gates 1–3, 5, 6), add a completed sign-off\n\
section with the reviewer's name, review date, gates attested, and status.\n\
Run `dkp validate` to confirm the conformance level after adding the sign-off.\n";
fn okf_index_md(name: &str, today: &str) -> String {
format!(
"---\n\
type: bundle-index\n\
pack: \"{name}\"\n\
generated: \"{today}\"\n\
---\n\
\n\
# {name} — OKF Bundle Index\n\
\n\
This file is the OKF layer index for the **{name}** Domain Knowledge Pack.\n\
Run `dkp okf export` to populate the concept files in subdirectories.\n"
)
}
const KNOWLEDGE_GRAPH_JSON: &str = r#"{
"nodes": [],
"edges": []
}
"#;
fn handbook_md(name: &str, domain: &str) -> String {
format!(
"# {name} Handbook\n\
\n\
A comprehensive reference for the **{domain}** domain.\n\
\n\
## Introduction\n\
\n\
TODO: Introduce the domain and the purpose of this handbook.\n\
\n\
## Key Concepts\n\
\n\
TODO: Explain the foundational concepts a reader needs to understand.\n\
\n\
## Domain Rules\n\
\n\
TODO: Summarize the operational rules in human-readable prose.\n\
\n\
## Common Questions\n\
\n\
TODO: Address frequently asked questions in this domain.\n"
)
}
fn readme_md(name: &str, domain: &str) -> String {
format!(
"# {name}\n\
\n\
A Domain Knowledge Pack for the **{domain}** domain.\n\
\n\
## Quick Start\n\
\n\
```bash\n\
dkp info .\n\
dkp validate .\n\
dkp inject . --scope system-prompt\n\
dkp tui .\n\
dkp webui .\n\
```\n\
\n\
## Contents\n\
\n\
TODO: describe what's in this pack.\n"
)
}
fn changelog_md(today: &str) -> String {
format!(
"# Changelog\n\
\n\
## [0.1.0] - {today}\n\
\n\
- Initial release.\n"
)
}