1use std::borrow::Cow;
7use std::collections::{BTreeMap, BTreeSet};
8
9use serde::{Deserialize, Serialize};
10
11#[cfg(feature = "nenjo")]
12pub mod builtin;
13pub mod tools;
14
15pub trait KnowledgePackManifest: Send + Sync {
21 fn pack_id(&self) -> &str;
22 fn pack_version(&self) -> &str;
23 fn schema_version(&self) -> u32;
24 fn root_uri(&self) -> &str;
25 fn content_hash(&self) -> &str;
26 fn docs(&self) -> &[KnowledgeDocManifest];
27
28 fn read_doc_manifest(&self, path: &str) -> Option<&KnowledgeDocManifest> {
29 self.docs()
30 .iter()
31 .find(|doc| doc.id == path || doc.virtual_path == path || doc.source_path == path)
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct KnowledgePackManifestData {
42 pub pack_id: String,
43 pub pack_version: String,
44 pub schema_version: u32,
45 pub root_uri: String,
46 #[serde(default)]
47 pub content_hash: String,
48 pub docs: Vec<KnowledgeDocManifest>,
49}
50
51impl KnowledgePackManifest for KnowledgePackManifestData {
52 fn pack_id(&self) -> &str {
53 &self.pack_id
54 }
55
56 fn pack_version(&self) -> &str {
57 &self.pack_version
58 }
59
60 fn schema_version(&self) -> u32 {
61 self.schema_version
62 }
63
64 fn root_uri(&self) -> &str {
65 &self.root_uri
66 }
67
68 fn content_hash(&self) -> &str {
69 &self.content_hash
70 }
71
72 fn docs(&self) -> &[KnowledgeDocManifest] {
73 &self.docs
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct KnowledgeDocManifest {
83 pub id: String,
84 pub virtual_path: String,
85 pub source_path: String,
86 pub title: String,
87 pub summary: String,
88 pub description: Option<String>,
89 pub kind: KnowledgeDocKind,
90 pub authority: KnowledgeDocAuthority,
91 pub status: KnowledgeDocStatus,
92 pub tags: Vec<String>,
93 pub aliases: Vec<String>,
94 pub keywords: Vec<String>,
95 pub related: Vec<KnowledgeDocEdge>,
96 #[serde(default)]
97 pub size_bytes: i64,
98 #[serde(default)]
99 pub updated_at: String,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "snake_case")]
104pub enum KnowledgeDocEdgeType {
105 PartOf,
106 Defines,
107 Governs,
108 Classifies,
109 References,
110 DependsOn,
111 Extends,
112 RelatedTo,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum KnowledgeDocKind {
118 Guide,
119 Reference,
120 Taxonomy,
121 Domain,
122 Entity,
123 Policy,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum KnowledgeDocAuthority {
129 Canonical,
130 Supporting,
131 Pattern,
132 Reference,
133 Advisory,
134 Example,
135 Draft,
136 Deprecated,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case")]
141pub enum KnowledgeDocStatus {
142 Stable,
143 Draft,
144 Deprecated,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct KnowledgeDocEdge {
149 #[serde(rename = "type", alias = "edge_type")]
150 pub edge_type: KnowledgeDocEdgeType,
151 pub target: String,
152 pub description: Option<String>,
153}
154
155#[derive(Debug, Clone, Default, Serialize, Deserialize)]
156pub struct KnowledgeDocFilter {
157 pub tags: Vec<String>,
158 pub kind: Option<KnowledgeDocKind>,
159 pub authority: Option<KnowledgeDocAuthority>,
160 pub status: Option<KnowledgeDocStatus>,
161 pub path_prefix: Option<String>,
162 pub related_to: Option<String>,
163 pub edge_type: Option<KnowledgeDocEdgeType>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct KnowledgeDocRead {
168 pub manifest: KnowledgeDocManifest,
169 pub content: String,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct KnowledgeDocNeighbor {
174 pub target: String,
175 pub edges: Vec<KnowledgeDocNeighborEdge>,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179pub struct KnowledgeDocNeighborEdge {
180 pub edge_type: KnowledgeDocEdgeType,
181 pub source: String,
182 pub target: String,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub note: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct KnowledgeDocSearchHit {
189 pub id: String,
190 pub virtual_path: String,
191 pub title: String,
192 pub summary: String,
193 pub kind: KnowledgeDocKind,
194 pub authority: KnowledgeDocAuthority,
195 pub tags: Vec<String>,
196 pub score: usize,
197 pub matched: Vec<String>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub content: Option<String>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct KnowledgeDocTree {
204 pub root_uri: String,
205 pub entries: Vec<KnowledgeDocTreeEntry>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct KnowledgeDocTreeEntry {
210 pub path: String,
211 pub title: String,
212 pub kind: KnowledgeDocKind,
213 pub tags: Vec<String>,
214}
215
216enum SearchMode {
217 MetadataOnly,
218 FullText,
219}
220
221pub trait KnowledgePack: Send + Sync {
223 fn manifest(&self) -> &dyn KnowledgePackManifest;
224
225 fn doc_content(&self, manifest: &KnowledgeDocManifest) -> Option<Cow<'_, str>>;
226
227 fn list_tree(&self, prefix: Option<&str>) -> KnowledgeDocTree {
228 let mut entries: Vec<_> = self
229 .manifest()
230 .docs()
231 .iter()
232 .filter(|doc| {
233 prefix
234 .map(|prefix| doc.virtual_path.starts_with(prefix))
235 .unwrap_or(true)
236 })
237 .map(|doc| KnowledgeDocTreeEntry {
238 path: doc.virtual_path.clone(),
239 title: doc.title.clone(),
240 kind: doc.kind,
241 tags: doc.tags.clone(),
242 })
243 .collect();
244 entries.sort_by(|a, b| a.path.cmp(&b.path));
245 KnowledgeDocTree {
246 root_uri: self.manifest().root_uri().to_string(),
247 entries,
248 }
249 }
250
251 fn list_docs(&self, filter: KnowledgeDocFilter) -> Vec<&KnowledgeDocManifest> {
252 self.manifest()
253 .docs()
254 .iter()
255 .filter(|doc| matches_filter(self, doc, &filter))
256 .collect()
257 }
258
259 fn read_manifest(&self, path: &str) -> Option<&KnowledgeDocManifest> {
260 self.manifest().read_doc_manifest(path)
261 }
262
263 fn read_doc(&self, path: &str) -> Option<KnowledgeDocRead> {
264 let manifest = self.read_manifest(path)?.clone();
265 let content = self.doc_content(&manifest)?.into_owned();
266 Some(KnowledgeDocRead { manifest, content })
267 }
268
269 fn search_paths(&self, query: &str, filter: KnowledgeDocFilter) -> Vec<KnowledgeDocSearchHit> {
270 search_pack(self, query, filter, SearchMode::MetadataOnly)
271 }
272
273 fn search_docs(&self, query: &str, filter: KnowledgeDocFilter) -> Vec<KnowledgeDocSearchHit> {
274 search_pack(self, query, filter, SearchMode::FullText)
275 }
276
277 fn neighbors(
278 &self,
279 path: &str,
280 edge_type: Option<KnowledgeDocEdgeType>,
281 ) -> Vec<KnowledgeDocNeighbor> {
282 let Some(source) = self.read_manifest(path) else {
283 return Vec::new();
284 };
285
286 let mut neighbors: BTreeMap<String, KnowledgeDocNeighbor> = BTreeMap::new();
287
288 for edge in &source.related {
289 if let Some(expected) = edge_type
290 && edge.edge_type != expected
291 {
292 continue;
293 }
294 if let Some(target) = self.read_manifest(&edge.target) {
295 push_neighbor_edge(
296 &mut neighbors,
297 target.virtual_path.clone(),
298 KnowledgeDocNeighborEdge {
299 edge_type: edge.edge_type,
300 source: source.virtual_path.clone(),
301 target: target.virtual_path.clone(),
302 note: edge.description.clone(),
303 },
304 );
305 }
306 }
307
308 for candidate in self.manifest().docs() {
309 for edge in &candidate.related {
310 let points_to_source = self
311 .read_manifest(&edge.target)
312 .map(|target| {
313 target.id == source.id || target.virtual_path == source.virtual_path
314 })
315 .unwrap_or_else(|| {
316 edge.target == source.id || edge.target == source.virtual_path
317 });
318 if !points_to_source {
319 continue;
320 }
321 if let Some(expected) = edge_type
322 && edge.edge_type != expected
323 {
324 continue;
325 }
326 push_neighbor_edge(
327 &mut neighbors,
328 candidate.virtual_path.clone(),
329 KnowledgeDocNeighborEdge {
330 edge_type: edge.edge_type,
331 source: candidate.virtual_path.clone(),
332 target: source.virtual_path.clone(),
333 note: edge.description.clone(),
334 },
335 );
336 }
337 }
338
339 neighbors.into_values().collect()
340 }
341}
342
343impl KnowledgeDocEdgeType {
344 pub fn as_str(self) -> &'static str {
345 match self {
346 KnowledgeDocEdgeType::PartOf => "part_of",
347 KnowledgeDocEdgeType::Defines => "defines",
348 KnowledgeDocEdgeType::Governs => "governs",
349 KnowledgeDocEdgeType::Classifies => "classifies",
350 KnowledgeDocEdgeType::References => "references",
351 KnowledgeDocEdgeType::DependsOn => "depends_on",
352 KnowledgeDocEdgeType::Extends => "extends",
353 KnowledgeDocEdgeType::RelatedTo => "related_to",
354 }
355 }
356}
357
358impl KnowledgeDocKind {
359 pub fn as_str(self) -> &'static str {
360 match self {
361 KnowledgeDocKind::Guide => "guide",
362 KnowledgeDocKind::Reference => "reference",
363 KnowledgeDocKind::Taxonomy => "taxonomy",
364 KnowledgeDocKind::Domain => "domain",
365 KnowledgeDocKind::Entity => "entity",
366 KnowledgeDocKind::Policy => "policy",
367 }
368 }
369}
370
371impl KnowledgeDocAuthority {
372 pub fn as_str(self) -> &'static str {
373 match self {
374 KnowledgeDocAuthority::Canonical => "canonical",
375 KnowledgeDocAuthority::Supporting => "supporting",
376 KnowledgeDocAuthority::Pattern => "pattern",
377 KnowledgeDocAuthority::Reference => "reference",
378 KnowledgeDocAuthority::Advisory => "advisory",
379 KnowledgeDocAuthority::Example => "example",
380 KnowledgeDocAuthority::Draft => "draft",
381 KnowledgeDocAuthority::Deprecated => "deprecated",
382 }
383 }
384}
385
386impl KnowledgeDocStatus {
387 pub fn as_str(self) -> &'static str {
388 match self {
389 KnowledgeDocStatus::Stable => "stable",
390 KnowledgeDocStatus::Draft => "draft",
391 KnowledgeDocStatus::Deprecated => "deprecated",
392 }
393 }
394}
395
396fn search_pack<P: KnowledgePack + ?Sized>(
397 pack: &P,
398 query: &str,
399 filter: KnowledgeDocFilter,
400 mode: SearchMode,
401) -> Vec<KnowledgeDocSearchHit> {
402 let needle = normalize(query);
403 let mut hits = Vec::new();
404
405 for manifest in pack.list_docs(filter) {
406 let mut score = 0;
407 let mut matched = BTreeSet::new();
408
409 score += score_field(&needle, &manifest.id, 100, "id", &mut matched);
410 score += score_field(
411 &needle,
412 &manifest.virtual_path,
413 90,
414 "virtual_path",
415 &mut matched,
416 );
417 score += score_field(&needle, &manifest.title, 80, "title", &mut matched);
418 score += score_field(&needle, &manifest.summary, 60, "summary", &mut matched);
419
420 for alias in &manifest.aliases {
421 score += score_field(&needle, alias, 75, "alias", &mut matched);
422 }
423 for tag in &manifest.tags {
424 score += score_field(&needle, tag, 70, "tag", &mut matched);
425 }
426 for keyword in &manifest.keywords {
427 score += score_field(&needle, keyword, 65, "keyword", &mut matched);
428 }
429
430 let content = match mode {
431 SearchMode::MetadataOnly => None,
432 SearchMode::FullText => pack.doc_content(manifest),
433 };
434 if let Some(content) = content.as_ref() {
435 score += score_field(&needle, content, 20, "content", &mut matched);
436 }
437
438 if score > 0 || needle.is_empty() {
439 hits.push(KnowledgeDocSearchHit {
440 id: manifest.id.clone(),
441 virtual_path: manifest.virtual_path.clone(),
442 title: manifest.title.clone(),
443 summary: manifest.summary.clone(),
444 kind: manifest.kind,
445 authority: manifest.authority,
446 tags: manifest.tags.clone(),
447 score,
448 matched: matched.into_iter().collect(),
449 content: matches!(mode, SearchMode::FullText)
450 .then(|| content.map(Cow::into_owned).unwrap_or_default()),
451 });
452 }
453 }
454
455 hits.sort_by(|a, b| {
456 b.score
457 .cmp(&a.score)
458 .then_with(|| a.virtual_path.cmp(&b.virtual_path))
459 });
460 hits
461}
462
463fn matches_filter<P: KnowledgePack + ?Sized>(
464 pack: &P,
465 doc: &KnowledgeDocManifest,
466 filter: &KnowledgeDocFilter,
467) -> bool {
468 if let Some(kind) = filter.kind
469 && doc.kind != kind
470 {
471 return false;
472 }
473 if let Some(authority) = filter.authority
474 && doc.authority != authority
475 {
476 return false;
477 }
478 if let Some(status) = filter.status
479 && doc.status != status
480 {
481 return false;
482 }
483 if let Some(prefix) = &filter.path_prefix
484 && !doc.virtual_path.starts_with(prefix)
485 {
486 return false;
487 }
488 if !filter.tags.is_empty()
489 && !filter
490 .tags
491 .iter()
492 .all(|tag| doc.tags.iter().any(|doc_tag| doc_tag == tag))
493 {
494 return false;
495 }
496 if let Some(target) = &filter.related_to {
497 let has_edge = doc.related.iter().any(|edge| {
498 let edge_matches_target = edge.target == *target
499 || pack
500 .read_manifest(&edge.target)
501 .map(|edge_target| {
502 edge_target.id == *target || edge_target.virtual_path == *target
503 })
504 .unwrap_or(false);
505 edge_matches_target
506 && filter
507 .edge_type
508 .as_ref()
509 .map(|expected| edge.edge_type == *expected)
510 .unwrap_or(true)
511 });
512 if !has_edge {
513 return false;
514 }
515 }
516 true
517}
518
519fn push_neighbor_edge(
520 neighbors: &mut BTreeMap<String, KnowledgeDocNeighbor>,
521 neighbor_target: String,
522 edge: KnowledgeDocNeighborEdge,
523) {
524 let neighbor =
525 neighbors
526 .entry(neighbor_target.clone())
527 .or_insert_with(|| KnowledgeDocNeighbor {
528 target: neighbor_target,
529 edges: Vec::new(),
530 });
531 if !neighbor.edges.contains(&edge) {
532 neighbor.edges.push(edge);
533 neighbor.edges.sort_by(|left, right| {
534 left.source
535 .cmp(&right.source)
536 .then_with(|| left.target.cmp(&right.target))
537 .then_with(|| left.edge_type.as_str().cmp(right.edge_type.as_str()))
538 .then_with(|| left.note.cmp(&right.note))
539 });
540 }
541}
542
543fn score_field(
544 needle: &str,
545 haystack: &str,
546 weight: usize,
547 label: &str,
548 matched: &mut BTreeSet<String>,
549) -> usize {
550 if needle.is_empty() {
551 return 1;
552 }
553 let haystack = normalize(haystack);
554 if haystack == needle {
555 matched.insert(label.to_string());
556 weight * 2
557 } else if haystack.contains(needle) {
558 matched.insert(label.to_string());
559 weight
560 } else {
561 0
562 }
563}
564
565fn normalize(value: &str) -> String {
566 value.trim().to_lowercase()
567}