use std::path::{Path, PathBuf};
use crate::{
error::{DkpError, DkpResult},
types::{
chunks::RetrievalChunk, constraints::ConstraintsFile, decision_trees::DecisionTreesFile,
eval::EvalCase, glossary::GlossaryFile, graph::KnowledgeGraph, manifest::Manifest,
ontology::OntologyFile, rules::RulesFile,
},
};
#[derive(Debug)]
pub struct Pack {
pub root: PathBuf,
pub manifest: Manifest,
}
impl Pack {
pub fn open(path: impl AsRef<Path>) -> DkpResult<Self> {
let root = path.as_ref().to_path_buf();
if !root.exists() {
return Err(DkpError::PackNotFound(root));
}
let manifest_path = root.join("manifest.json");
if !manifest_path.exists() {
return Err(DkpError::ManifestMissing(root));
}
let bytes = std::fs::read(&manifest_path)?;
let manifest: Manifest =
serde_json::from_slice(&bytes).map_err(|e| DkpError::ManifestInvalid {
reason: e.to_string(),
})?;
validate_required_fields(&manifest)?;
Ok(Pack { root, manifest })
}
pub fn machine_dir(&self) -> PathBuf {
self.root.join("machine")
}
pub fn okf_dir(&self) -> PathBuf {
self.root.join("okf")
}
pub fn human_dir(&self) -> PathBuf {
self.root.join("human")
}
pub fn evidence_dir(&self) -> PathBuf {
self.root.join("evidence")
}
pub fn skills_dir(&self) -> PathBuf {
self.root.join("skills")
}
pub fn l10n_dir(&self) -> PathBuf {
self.root.join("l10n")
}
pub fn build_dir(&self) -> PathBuf {
self.root.join("build")
}
pub fn procedures_dir(&self) -> PathBuf {
self.machine_dir().join("procedures")
}
pub fn wasm_procedures_dir(&self) -> PathBuf {
self.machine_dir().join("procedures").join("src")
}
pub fn machine_file(&self, name: &str) -> PathBuf {
self.machine_dir().join(name)
}
pub fn evidence_file(&self, name: &str) -> PathBuf {
self.evidence_dir().join(name)
}
pub fn has_okf(&self) -> bool {
self.okf_dir().exists()
}
pub fn has_procedures(&self) -> bool {
self.procedures_dir().exists()
}
pub fn has_bundle_sig(&self) -> bool {
self.root.join("bundle.sig").exists()
}
pub fn has_eval_set(&self) -> bool {
self.machine_file("eval_set.jsonl").exists()
}
pub fn has_knowledge_graph(&self) -> bool {
self.machine_file("knowledge_graph.json").exists()
}
pub fn has_mcp_manifest(&self) -> bool {
self.machine_file("mcp_manifest.json").exists()
}
pub fn has_skills(&self) -> bool {
self.skills_dir().exists()
}
pub fn has_l10n(&self) -> bool {
self.l10n_dir().exists()
}
pub fn has_cross_refs(&self) -> bool {
self.machine_file("cross_refs.json").exists()
}
pub fn has_assets(&self) -> bool {
self.machine_file("assets.json").exists()
}
pub fn has_checksums(&self) -> bool {
self.root.join("checksums.json").exists()
}
pub fn mcp_enabled(&self) -> bool {
self.manifest.mcp.is_some()
}
pub fn load_glossary(&self) -> DkpResult<Option<GlossaryFile>> {
load_json_optional(self, "glossary.json")
}
pub fn load_rules(&self) -> DkpResult<Option<RulesFile>> {
load_json_optional(self, "rules.json")
}
pub fn load_ontology(&self) -> DkpResult<Option<OntologyFile>> {
load_json_optional(self, "ontology.json")
}
pub fn load_constraints(&self) -> DkpResult<Option<ConstraintsFile>> {
load_json_optional(self, "constraints.json")
}
pub fn load_decision_trees(&self) -> DkpResult<Option<DecisionTreesFile>> {
load_json_optional(self, "decision_trees.json")
}
pub fn load_graph(&self) -> DkpResult<Option<KnowledgeGraph>> {
load_json_optional(self, "knowledge_graph.json")
}
pub fn load_chunks(&self) -> DkpResult<Vec<RetrievalChunk>> {
load_jsonl(self, "retrieval_chunks.jsonl")
}
pub fn load_eval_set(&self) -> DkpResult<Vec<EvalCase>> {
load_jsonl(self, "eval_set.jsonl")
}
pub fn load_system_prompt(&self) -> DkpResult<Option<String>> {
let path = self.machine_file("system_prompt.md");
if !path.exists() {
return Ok(None);
}
Ok(Some(std::fs::read_to_string(path)?))
}
}
fn validate_required_fields(m: &Manifest) -> DkpResult<()> {
macro_rules! require {
($field:expr, $name:literal) => {
if $field.trim().is_empty() {
return Err(DkpError::ManifestFieldMissing { field: $name });
}
};
}
require!(m.spec, "spec");
require!(m.name, "name");
require!(m.version, "version");
require!(m.domain, "domain");
require!(m.audience, "audience");
require!(m.intended_use, "intended_use");
require!(m.known_limitations, "known_limitations");
require!(m.update_date, "update_date");
if let Err(e) = crate::domain::derive_and_validate_domain_slug(&m.domain) {
return Err(DkpError::ManifestDomainInvalid {
domain: m.domain.clone(),
reason: e.to_string(),
});
}
Ok(())
}
fn load_json_optional<T: serde::de::DeserializeOwned>(
pack: &Pack,
filename: &str,
) -> DkpResult<Option<T>> {
let path = pack.machine_file(filename);
if !path.exists() {
return Ok(None);
}
let bytes = std::fs::read(&path)?;
let value = serde_json::from_slice(&bytes).map_err(|e| DkpError::AssetParse {
asset: filename.to_string(),
source: e,
})?;
Ok(Some(value))
}
fn load_jsonl<T: serde::de::DeserializeOwned>(pack: &Pack, filename: &str) -> DkpResult<Vec<T>> {
let path = pack.machine_file(filename);
if !path.exists() {
return Ok(vec![]);
}
let content = std::fs::read_to_string(&path)?;
let mut items = Vec::new();
let mut errors = Vec::new();
for (i, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
match serde_json::from_str::<T>(line) {
Ok(item) => items.push(item),
Err(e) => errors.push(DkpError::JsonlParse {
file: filename.to_string(),
line: i + 1,
reason: e.to_string(),
}),
}
}
if !errors.is_empty() {
return Err(errors.remove(0));
}
Ok(items)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn minimal_manifest_json() -> &'static str {
r#"{
"spec": "1.0.0",
"name": "test-pack",
"version": "1.0.0",
"domain": "testing",
"audience": "internal",
"intended_use": "unit tests",
"known_limitations": "none",
"update_date": "2026-01-01"
}"#
}
fn write_manifest(dir: &std::path::Path, contents: &str) {
std::fs::write(dir.join("manifest.json"), contents).unwrap();
}
#[test]
fn open_nonexistent_dir_errors() {
let tmp = TempDir::new().unwrap();
let missing = tmp.path().join("does-not-exist");
let err = Pack::open(&missing).unwrap_err();
assert!(matches!(err, DkpError::PackNotFound(_)));
}
#[test]
fn open_missing_manifest_errors() {
let tmp = TempDir::new().unwrap();
let err = Pack::open(tmp.path()).unwrap_err();
assert!(matches!(err, DkpError::ManifestMissing(_)));
}
#[test]
fn open_invalid_manifest_json_errors() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), "{ not valid json");
let err = Pack::open(tmp.path()).unwrap_err();
assert!(matches!(err, DkpError::ManifestInvalid { .. }));
}
#[test]
fn open_manifest_missing_required_field_errors() {
let tmp = TempDir::new().unwrap();
write_manifest(
tmp.path(),
r#"{
"spec": "1.0.0",
"name": "",
"version": "1.0.0",
"domain": "testing",
"audience": "internal",
"intended_use": "unit tests",
"known_limitations": "none",
"update_date": "2026-01-01"
}"#,
);
let err = Pack::open(tmp.path()).unwrap_err();
assert!(matches!(
err,
DkpError::ManifestFieldMissing { field: "name" }
));
}
#[test]
fn open_manifest_invalid_domain_errors() {
let tmp = TempDir::new().unwrap();
write_manifest(
tmp.path(),
r#"{
"spec": "1.0.0",
"name": "test-pack",
"version": "1.0.0",
"domain": "!!!",
"audience": "internal",
"intended_use": "unit tests",
"known_limitations": "none",
"update_date": "2026-01-01"
}"#,
);
let err = Pack::open(tmp.path()).unwrap_err();
assert!(matches!(err, DkpError::ManifestDomainInvalid { .. }));
}
#[test]
fn open_manifest_reserved_word_domain_succeeds_locally() {
let tmp = TempDir::new().unwrap();
write_manifest(
tmp.path(),
r#"{
"spec": "1.0.0",
"name": "test-pack",
"version": "1.0.0",
"domain": "Admin",
"audience": "internal",
"intended_use": "unit tests",
"known_limitations": "none",
"update_date": "2026-01-01"
}"#,
);
Pack::open(tmp.path()).expect("reserved-word domains are allowed for local builds");
}
#[test]
fn open_valid_minimal_manifest_succeeds() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), minimal_manifest_json());
let pack = Pack::open(tmp.path()).unwrap();
assert_eq!(pack.manifest.name, "test-pack");
assert_eq!(pack.manifest.domain, "testing");
assert_eq!(pack.root, tmp.path());
}
#[test]
fn has_helpers_reflect_filesystem_state() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), minimal_manifest_json());
let pack = Pack::open(tmp.path()).unwrap();
assert!(!pack.has_okf());
assert!(!pack.has_procedures());
assert!(!pack.has_bundle_sig());
assert!(!pack.has_eval_set());
assert!(!pack.has_knowledge_graph());
assert!(!pack.has_mcp_manifest());
assert!(!pack.has_skills());
assert!(!pack.has_l10n());
assert!(!pack.has_cross_refs());
assert!(!pack.has_assets());
assert!(!pack.has_checksums());
assert!(!pack.mcp_enabled());
std::fs::create_dir_all(pack.okf_dir()).unwrap();
std::fs::create_dir_all(pack.procedures_dir()).unwrap();
std::fs::write(pack.root.join("bundle.sig"), "sig").unwrap();
std::fs::write(pack.machine_file("eval_set.jsonl"), "").unwrap();
std::fs::write(pack.machine_file("knowledge_graph.json"), "{}").unwrap();
std::fs::write(pack.machine_file("mcp_manifest.json"), "{}").unwrap();
std::fs::create_dir_all(pack.skills_dir()).unwrap();
std::fs::create_dir_all(pack.l10n_dir()).unwrap();
std::fs::write(pack.machine_file("cross_refs.json"), "{}").unwrap();
std::fs::write(pack.machine_file("assets.json"), "{}").unwrap();
std::fs::write(pack.root.join("checksums.json"), "{}").unwrap();
assert!(pack.has_okf());
assert!(pack.has_procedures());
assert!(pack.has_bundle_sig());
assert!(pack.has_eval_set());
assert!(pack.has_knowledge_graph());
assert!(pack.has_mcp_manifest());
assert!(pack.has_skills());
assert!(pack.has_l10n());
assert!(pack.has_cross_refs());
assert!(pack.has_assets());
assert!(pack.has_checksums());
}
#[test]
fn load_json_optional_wrappers_absent_return_none() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), minimal_manifest_json());
let pack = Pack::open(tmp.path()).unwrap();
assert!(pack.load_glossary().unwrap().is_none());
assert!(pack.load_rules().unwrap().is_none());
assert!(pack.load_ontology().unwrap().is_none());
assert!(pack.load_constraints().unwrap().is_none());
assert!(pack.load_decision_trees().unwrap().is_none());
assert!(pack.load_graph().unwrap().is_none());
assert!(pack.load_system_prompt().unwrap().is_none());
}
#[test]
fn load_glossary_valid_returns_some() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), minimal_manifest_json());
let pack = Pack::open(tmp.path()).unwrap();
std::fs::create_dir_all(pack.machine_dir()).unwrap();
std::fs::write(
pack.machine_file("glossary.json"),
r#"{"terms": [{"id": "t1", "term": "Term", "definition": "def"}]}"#,
)
.unwrap();
let glossary = pack.load_glossary().unwrap().unwrap();
assert_eq!(glossary.terms.len(), 1);
assert_eq!(glossary.terms[0].id, "t1");
}
#[test]
fn load_glossary_invalid_json_errors() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), minimal_manifest_json());
let pack = Pack::open(tmp.path()).unwrap();
std::fs::create_dir_all(pack.machine_dir()).unwrap();
std::fs::write(pack.machine_file("glossary.json"), "{ bad json").unwrap();
let err = pack.load_glossary().unwrap_err();
assert!(matches!(err, DkpError::AssetParse { .. }));
}
#[test]
fn load_chunks_absent_returns_empty_vec() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), minimal_manifest_json());
let pack = Pack::open(tmp.path()).unwrap();
assert!(pack.load_chunks().unwrap().is_empty());
}
#[test]
fn load_chunks_valid_jsonl() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), minimal_manifest_json());
let pack = Pack::open(tmp.path()).unwrap();
std::fs::create_dir_all(pack.machine_dir()).unwrap();
let jsonl = concat!(
r#"{"id":"c1","title":"T1","chunk_text":"body one","source_ref":"generated"}"#,
"\n",
r#"{"id":"c2","title":"T2","chunk_text":"body two","source_ref":"generated"}"#,
"\n",
);
std::fs::write(pack.machine_file("retrieval_chunks.jsonl"), jsonl).unwrap();
let chunks = pack.load_chunks().unwrap();
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].id, "c1");
assert_eq!(chunks[1].id, "c2");
}
#[test]
fn load_chunks_invalid_line_errors() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), minimal_manifest_json());
let pack = Pack::open(tmp.path()).unwrap();
std::fs::create_dir_all(pack.machine_dir()).unwrap();
std::fs::write(pack.machine_file("retrieval_chunks.jsonl"), "not json\n").unwrap();
let err = pack.load_chunks().unwrap_err();
assert!(matches!(err, DkpError::JsonlParse { .. }));
}
#[test]
fn load_system_prompt_present() {
let tmp = TempDir::new().unwrap();
write_manifest(tmp.path(), minimal_manifest_json());
let pack = Pack::open(tmp.path()).unwrap();
std::fs::create_dir_all(pack.machine_dir()).unwrap();
std::fs::write(pack.machine_file("system_prompt.md"), "Be helpful.").unwrap();
assert_eq!(
pack.load_system_prompt().unwrap(),
Some("Be helpful.".to_string())
);
}
}