use std::sync::LazyLock;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const CATALOG_PATH: &str = "instance/docs-catalog.toml";
pub const SCHEMA: u32 = 1;
pub const JSON_SCHEMA: &str = "sdd.docs/1";
#[derive(Debug, Error, PartialEq, Eq)]
pub enum CatalogError {
#[error("{CATALOG_PATH} does not parse: {0}")]
Malformed(String),
#[error("{CATALOG_PATH} declares schema {0}, and this engine reads {SCHEMA}")]
UnknownSchema(u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ShelfId {
Method,
Spec,
Template,
}
impl ShelfId {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Method => "method",
Self::Spec => "spec",
Self::Template => "template",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum Target {
Document {
shelf: ShelfId,
name: String,
},
Command(Vec<String>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Kind {
MethodChapter,
SpecSeed,
Template,
OperatorTask,
}
impl Kind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::MethodChapter => "method-chapter",
Self::SpecSeed => "spec-seed",
Self::Template => "template",
Self::OperatorTask => "operator-task",
}
}
#[must_use]
pub const fn heading(self) -> &'static str {
match self {
Self::MethodChapter => "Method chapters",
Self::SpecSeed => "Specifications",
Self::Template => "Templates",
Self::OperatorTask => "Operator tasks",
}
}
#[must_use]
pub const fn every() -> [Self; 4] {
[
Self::OperatorTask,
Self::MethodChapter,
Self::SpecSeed,
Self::Template,
]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Topic {
pub id: String,
pub title: String,
pub summary: String,
pub kind: Kind,
#[serde(default)]
pub aliases: Vec<String>,
pub target: Target,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Catalog {
pub catalog_schema: u32,
#[serde(default, rename = "topic")]
pub topics: Vec<Topic>,
}
#[expect(
clippy::expect_used,
reason = "the catalog is compiled in; a parse failure is a build defect the canon suite catches first"
)]
pub static CATALOG: LazyLock<Catalog> = LazyLock::new(|| {
let bytes = crate::embedded::asset(CATALOG_PATH)
.expect("the payload carries instance/docs-catalog.toml");
Catalog::parse(bytes).expect("the embedded documentation catalog parses")
});
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum Unresolved {
#[error("'{query}' matches {}; name one", .candidates.join(", "))]
Ambiguous {
query: String,
candidates: Vec<String>,
},
#[error("no topic matches '{query}'; nearest: {}", .nearest.join(", "))]
Missing {
query: String,
nearest: Vec<String>,
},
}
fn fold(value: &str) -> String {
value
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase()
}
impl Catalog {
pub fn parse(bytes: &[u8]) -> Result<Self, CatalogError> {
let text = std::str::from_utf8(bytes)
.map_err(|source| CatalogError::Malformed(source.to_string()))?;
let held: Self =
toml::from_str(text).map_err(|source| CatalogError::Malformed(source.to_string()))?;
if held.catalog_schema != SCHEMA {
return Err(CatalogError::UnknownSchema(held.catalog_schema));
}
Ok(held)
}
#[must_use]
pub fn for_document(&self, on: ShelfId, wanted: &str) -> Option<&Topic> {
self.topics.iter().find(|topic| {
matches!(&topic.target, Target::Document { shelf: held, name: held_name }
if *held == on && held_name == wanted)
})
}
pub fn resolve(&self, query: &str) -> Result<&Topic, Unresolved> {
let wanted = fold(query);
if let Some(topic) = self.topics.iter().find(|topic| fold(&topic.id) == wanted) {
return Ok(topic);
}
let by_alias: Vec<&Topic> = self
.topics
.iter()
.filter(|topic| topic.aliases.iter().any(|alias| fold(alias) == wanted))
.collect();
if let [only] = by_alias.as_slice() {
return Ok(only);
}
if !by_alias.is_empty() {
return Err(Unresolved::Ambiguous {
query: wanted,
candidates: by_alias.iter().map(|topic| topic.id.clone()).collect(),
});
}
let by_prefix: Vec<&Topic> = self
.topics
.iter()
.filter(|topic| {
fold(&topic.id).starts_with(&wanted)
|| topic
.aliases
.iter()
.any(|alias| fold(alias).starts_with(&wanted))
})
.collect();
match by_prefix.as_slice() {
[only] => Ok(only),
[] => Err(Unresolved::Missing {
query: wanted.clone(),
nearest: self.nearest(&wanted),
}),
many => Err(Unresolved::Ambiguous {
query: wanted,
candidates: many.iter().map(|topic| topic.id.clone()).collect(),
}),
}
}
fn nearest(&self, wanted: &str) -> Vec<String> {
let mut near: Vec<String> = self
.topics
.iter()
.filter(|topic| {
let id = fold(&topic.id);
id.contains(wanted) || wanted.contains(&id)
})
.map(|topic| topic.id.clone())
.collect();
if near.is_empty() {
near = self.topics.iter().map(|topic| topic.id.clone()).collect();
}
near.truncate(8);
near
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
reason = "a test panics as its failure signal, not as control flow"
)]
use super::*;
const SAMPLE: &str = r#"
catalog_schema = 1
[[topic]]
id = "agent-context"
title = "Agent context"
summary = "What an agent loads and the budget an always-loaded file pays."
kind = "method-chapter"
aliases = ["context budget", "always loaded"]
target = { document = { shelf = "method", name = "agent-context" } }
[[topic]]
id = "agents-digest"
title = "The digest template"
summary = "A stable starting point for an author-instructions file."
kind = "template"
aliases = ["digest template"]
target = { document = { shelf = "template", name = "agents-digest" } }
[[topic]]
id = "upgrade"
title = "Upgrading an instance"
summary = "Take a landed instance to a chosen release through one plan."
kind = "operator-task"
aliases = ["new version"]
target = { command = ["reconcile", "plan", "--help"] }
"#;
fn sample() -> Catalog {
Catalog::parse(SAMPLE.as_bytes()).unwrap()
}
#[test]
fn a_topic_resolves_by_id_by_alias_and_by_unique_prefix() {
let catalog = sample();
assert_eq!(
catalog.resolve("agent-context").unwrap().id,
"agent-context"
);
assert_eq!(
catalog.resolve("Context Budget").unwrap().id,
"agent-context"
);
assert_eq!(catalog.resolve("upg").unwrap().id, "upgrade");
assert_eq!(
catalog.resolve("always loaded").unwrap().id,
"agent-context"
);
}
#[test]
fn an_ambiguous_query_names_every_candidate_and_guesses_none() {
let catalog = sample();
let refused = catalog.resolve("agent").unwrap_err();
let Unresolved::Ambiguous { candidates, .. } = refused else {
panic!("an ambiguous prefix resolved to one topic");
};
assert_eq!(candidates, vec!["agent-context", "agents-digest"]);
}
#[test]
fn a_missing_topic_names_the_nearest_ids() {
let catalog = sample();
let refused = catalog.resolve("release process").unwrap_err();
let Unresolved::Missing { nearest, .. } = refused else {
panic!("a missing topic did not report as missing");
};
assert!(!nearest.is_empty());
}
#[test]
fn an_unknown_schema_or_shape_refuses() {
assert!(matches!(
Catalog::parse(b"catalog_schema = 9\n").unwrap_err(),
CatalogError::UnknownSchema(9)
));
assert!(matches!(
Catalog::parse(b"catalog_schema = 1\nextra = 1\n").unwrap_err(),
CatalogError::Malformed(_)
));
}
#[test]
fn a_document_topic_is_found_by_its_shelf_and_name() {
let catalog = sample();
assert_eq!(
catalog
.for_document(ShelfId::Method, "agent-context")
.unwrap()
.id,
"agent-context"
);
assert!(
catalog
.for_document(ShelfId::Spec, "agent-context")
.is_none()
);
}
}