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
121 .mcp
122 .as_ref()
123 .map(|m| m.enabled)
124 .unwrap_or(false)
125 }
126
127 pub fn load_glossary(&self) -> DkpResult<Option<GlossaryFile>> {
130 load_json_optional(self, "glossary.json")
131 }
132
133 pub fn load_rules(&self) -> DkpResult<Option<RulesFile>> {
134 load_json_optional(self, "rules.json")
135 }
136
137 pub fn load_ontology(&self) -> DkpResult<Option<OntologyFile>> {
138 load_json_optional(self, "ontology.json")
139 }
140
141 pub fn load_constraints(&self) -> DkpResult<Option<ConstraintsFile>> {
142 load_json_optional(self, "constraints.json")
143 }
144
145 pub fn load_decision_trees(&self) -> DkpResult<Option<DecisionTreesFile>> {
146 load_json_optional(self, "decision_trees.json")
147 }
148
149 pub fn load_graph(&self) -> DkpResult<Option<KnowledgeGraph>> {
150 load_json_optional(self, "knowledge_graph.json")
151 }
152
153 pub fn load_chunks(&self) -> DkpResult<Vec<RetrievalChunk>> {
154 load_jsonl(self, "retrieval_chunks.jsonl")
155 }
156
157 pub fn load_eval_set(&self) -> DkpResult<Vec<EvalCase>> {
158 load_jsonl(self, "eval_set.jsonl")
159 }
160
161 pub fn load_system_prompt(&self) -> DkpResult<Option<String>> {
162 let path = self.machine_file("system_prompt.md");
163 if !path.exists() {
164 return Ok(None);
165 }
166 Ok(Some(std::fs::read_to_string(path)?))
167 }
168}
169
170fn validate_required_fields(m: &Manifest) -> DkpResult<()> {
173 macro_rules! require {
174 ($field:expr, $name:literal) => {
175 if $field.trim().is_empty() {
176 return Err(DkpError::ManifestFieldMissing { field: $name });
177 }
178 };
179 }
180 require!(m.spec, "spec");
181 require!(m.name, "name");
182 require!(m.version, "version");
183 require!(m.domain, "domain");
184 require!(m.audience, "audience");
185 require!(m.intended_use, "intended_use");
186 require!(m.known_limitations, "known_limitations");
187 require!(m.update_date, "update_date");
188 Ok(())
189}
190
191fn load_json_optional<T: serde::de::DeserializeOwned>(
192 pack: &Pack,
193 filename: &str,
194) -> DkpResult<Option<T>> {
195 let path = pack.machine_file(filename);
196 if !path.exists() {
197 return Ok(None);
198 }
199 let bytes = std::fs::read(&path)?;
200 let value = serde_json::from_slice(&bytes).map_err(|e| DkpError::AssetParse {
201 asset: filename.to_string(),
202 source: e,
203 })?;
204 Ok(Some(value))
205}
206
207fn load_jsonl<T: serde::de::DeserializeOwned>(pack: &Pack, filename: &str) -> DkpResult<Vec<T>> {
208 let path = pack.machine_file(filename);
209 if !path.exists() {
210 return Ok(vec![]);
211 }
212
213 let content = std::fs::read_to_string(&path)?;
214 let mut items = Vec::new();
215 let mut errors = Vec::new();
216
217 for (i, line) in content.lines().enumerate() {
218 let line = line.trim();
219 if line.is_empty() {
220 continue;
221 }
222 match serde_json::from_str::<T>(line) {
223 Ok(item) => items.push(item),
224 Err(e) => errors.push(DkpError::JsonlParse {
225 file: filename.to_string(),
226 line: i + 1,
227 reason: e.to_string(),
228 }),
229 }
230 }
231
232 if !errors.is_empty() {
233 return Err(errors.remove(0));
234 }
235
236 Ok(items)
237}