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
12pub struct Pack {
17 pub root: PathBuf,
18 pub manifest: Manifest,
19}
20
21impl Pack {
22 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 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 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 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
166fn 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}