Skip to main content

dkp_core/pack/
loader.rs

1use std::path::{Path, PathBuf};
2
3use crate::{
4    error::{DkpError, DkpResult},
5    types::{
6        chunks::RetrievalChunk, constraints::ConstraintsFile, decision_trees::DecisionTreesFile,
7        eval::EvalCase, glossary::GlossaryFile, graph::KnowledgeGraph, manifest::Manifest,
8        ontology::OntologyFile, rules::RulesFile,
9    },
10};
11
12/// Lightweight handle to an open DKP pack directory.
13///
14/// `Pack::open()` reads only `manifest.json`. All other assets are loaded
15/// lazily by the commands that need them.
16pub struct Pack {
17    pub root: PathBuf,
18    pub manifest: Manifest,
19}
20
21impl Pack {
22    /// Open a pack directory or `.zip` archive. Reads and validates manifest.json.
23    pub fn open(path: impl AsRef<Path>) -> DkpResult<Self> {
24        let root = path.as_ref().to_path_buf();
25
26        if !root.exists() {
27            return Err(DkpError::PackNotFound(root));
28        }
29
30        let manifest_path = root.join("manifest.json");
31        if !manifest_path.exists() {
32            return Err(DkpError::ManifestMissing(root));
33        }
34
35        let bytes = std::fs::read(&manifest_path)?;
36        let manifest: Manifest =
37            serde_json::from_slice(&bytes).map_err(|e| DkpError::ManifestInvalid {
38                reason: e.to_string(),
39            })?;
40
41        validate_required_fields(&manifest)?;
42
43        Ok(Pack { root, manifest })
44    }
45
46    // ── Directory helpers ────────────────────────────────────────────────────
47
48    pub fn machine_dir(&self) -> PathBuf {
49        self.root.join("machine")
50    }
51    pub fn okf_dir(&self) -> PathBuf {
52        self.root.join("okf")
53    }
54    pub fn human_dir(&self) -> PathBuf {
55        self.root.join("human")
56    }
57    pub fn evidence_dir(&self) -> PathBuf {
58        self.root.join("evidence")
59    }
60    pub fn skills_dir(&self) -> PathBuf {
61        self.root.join("skills")
62    }
63    pub fn l10n_dir(&self) -> PathBuf {
64        self.root.join("l10n")
65    }
66    pub fn build_dir(&self) -> PathBuf {
67        self.root.join("build")
68    }
69    pub fn procedures_dir(&self) -> PathBuf {
70        self.machine_dir().join("procedures")
71    }
72    pub fn wasm_procedures_dir(&self) -> PathBuf {
73        self.machine_dir().join("procedures").join("src")
74    }
75
76    pub fn machine_file(&self, name: &str) -> PathBuf {
77        self.machine_dir().join(name)
78    }
79    pub fn evidence_file(&self, name: &str) -> PathBuf {
80        self.evidence_dir().join(name)
81    }
82
83    // ── Presence checks ──────────────────────────────────────────────────────
84
85    pub fn has_okf(&self) -> bool {
86        self.okf_dir().exists()
87    }
88    pub fn has_procedures(&self) -> bool {
89        self.procedures_dir().exists()
90    }
91    pub fn has_bundle_sig(&self) -> bool {
92        self.root.join("bundle.sig").exists()
93    }
94    pub fn has_eval_set(&self) -> bool {
95        self.machine_file("eval_set.jsonl").exists()
96    }
97    pub fn has_knowledge_graph(&self) -> bool {
98        self.machine_file("knowledge_graph.json").exists()
99    }
100    pub fn has_mcp_manifest(&self) -> bool {
101        self.machine_file("mcp_manifest.json").exists()
102    }
103    pub fn has_skills(&self) -> bool {
104        self.skills_dir().exists()
105    }
106    pub fn has_l10n(&self) -> bool {
107        self.l10n_dir().exists()
108    }
109    pub fn has_cross_refs(&self) -> bool {
110        self.machine_file("cross_refs.json").exists()
111    }
112    pub fn has_assets(&self) -> bool {
113        self.machine_file("assets.json").exists()
114    }
115    pub fn has_checksums(&self) -> bool {
116        self.root.join("checksums.json").exists()
117    }
118
119    pub fn mcp_enabled(&self) -> bool {
120        self.manifest.mcp.is_some()
121    }
122
123    // ── Asset loaders ────────────────────────────────────────────────────────
124
125    pub fn load_glossary(&self) -> DkpResult<Option<GlossaryFile>> {
126        load_json_optional(self, "glossary.json")
127    }
128
129    pub fn load_rules(&self) -> DkpResult<Option<RulesFile>> {
130        load_json_optional(self, "rules.json")
131    }
132
133    pub fn load_ontology(&self) -> DkpResult<Option<OntologyFile>> {
134        load_json_optional(self, "ontology.json")
135    }
136
137    pub fn load_constraints(&self) -> DkpResult<Option<ConstraintsFile>> {
138        load_json_optional(self, "constraints.json")
139    }
140
141    pub fn load_decision_trees(&self) -> DkpResult<Option<DecisionTreesFile>> {
142        load_json_optional(self, "decision_trees.json")
143    }
144
145    pub fn load_graph(&self) -> DkpResult<Option<KnowledgeGraph>> {
146        load_json_optional(self, "knowledge_graph.json")
147    }
148
149    pub fn load_chunks(&self) -> DkpResult<Vec<RetrievalChunk>> {
150        load_jsonl(self, "retrieval_chunks.jsonl")
151    }
152
153    pub fn load_eval_set(&self) -> DkpResult<Vec<EvalCase>> {
154        load_jsonl(self, "eval_set.jsonl")
155    }
156
157    pub fn load_system_prompt(&self) -> DkpResult<Option<String>> {
158        let path = self.machine_file("system_prompt.md");
159        if !path.exists() {
160            return Ok(None);
161        }
162        Ok(Some(std::fs::read_to_string(path)?))
163    }
164}
165
166// ── Private helpers ──────────────────────────────────────────────────────────
167
168fn validate_required_fields(m: &Manifest) -> DkpResult<()> {
169    macro_rules! require {
170        ($field:expr, $name:literal) => {
171            if $field.trim().is_empty() {
172                return Err(DkpError::ManifestFieldMissing { field: $name });
173            }
174        };
175    }
176    require!(m.spec, "spec");
177    require!(m.name, "name");
178    require!(m.version, "version");
179    require!(m.domain, "domain");
180    require!(m.audience, "audience");
181    require!(m.intended_use, "intended_use");
182    require!(m.known_limitations, "known_limitations");
183    require!(m.update_date, "update_date");
184    Ok(())
185}
186
187fn load_json_optional<T: serde::de::DeserializeOwned>(
188    pack: &Pack,
189    filename: &str,
190) -> DkpResult<Option<T>> {
191    let path = pack.machine_file(filename);
192    if !path.exists() {
193        return Ok(None);
194    }
195    let bytes = std::fs::read(&path)?;
196    let value = serde_json::from_slice(&bytes).map_err(|e| DkpError::AssetParse {
197        asset: filename.to_string(),
198        source: e,
199    })?;
200    Ok(Some(value))
201}
202
203fn load_jsonl<T: serde::de::DeserializeOwned>(pack: &Pack, filename: &str) -> DkpResult<Vec<T>> {
204    let path = pack.machine_file(filename);
205    if !path.exists() {
206        return Ok(vec![]);
207    }
208
209    let content = std::fs::read_to_string(&path)?;
210    let mut items = Vec::new();
211    let mut errors = Vec::new();
212
213    for (i, line) in content.lines().enumerate() {
214        let line = line.trim();
215        if line.is_empty() {
216            continue;
217        }
218        match serde_json::from_str::<T>(line) {
219            Ok(item) => items.push(item),
220            Err(e) => errors.push(DkpError::JsonlParse {
221                file: filename.to_string(),
222                line: i + 1,
223                reason: e.to_string(),
224            }),
225        }
226    }
227
228    if !errors.is_empty() {
229        return Err(errors.remove(0));
230    }
231
232    Ok(items)
233}