#![forbid(unsafe_code)]
use std::{
collections::{BTreeMap, HashMap, HashSet},
error, fmt,
};
use kcode_kweb_db::NodeId;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Error {
message: String,
}
impl Error {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl error::Error for Error {}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Connection {
pub id: String,
pub short_name: String,
pub short_description: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Node {
pub id: String,
pub short_name: String,
pub short_description: String,
pub long_description: String,
pub owner: String,
#[serde(default)]
pub fixed_connections: Vec<Connection>,
#[serde(default)]
pub recent_connections: Vec<Connection>,
#[serde(default)]
pub objects: Vec<String>,
#[serde(default)]
pub last_modified_by: String,
#[serde(default)]
pub last_modified_at: Option<String>,
}
impl Node {
pub fn from_kweb_value(value: &Value) -> Result<Self> {
let id = required_string(value, "id")?;
canonical_node_id(&id)?;
let owner = value
.get("owner_node_id")
.or_else(|| value.get("owner_root_node_id"))
.and_then(Value::as_str)
.unwrap_or("unowned")
.to_owned();
if !matches!(owner.as_str(), "self" | "unowned") {
canonical_node_id(&owner)?;
}
let summaries = value
.get("connection_summaries")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|summary| Some((summary.get("id")?.as_str()?.to_owned(), summary)))
.collect::<HashMap<_, _>>();
let fixed_connections = connections(
value.get("fixed_connections"),
&summaries,
"fixed connection",
)?;
let recent_connections = connections(
value.get("recent_connections"),
&summaries,
"recent connection",
)?;
let objects = string_ids(value.get("objects"), "object")?;
Ok(Self {
id,
short_name: optional_string(value, "short_name"),
short_description: optional_string(value, "short_description"),
long_description: optional_string(value, "long_description"),
owner,
fixed_connections,
recent_connections,
objects,
last_modified_by: optional_string(value, "last_modified_by"),
last_modified_at: value
.get("last_modified_at")
.and_then(Value::as_str)
.map(str::to_owned),
})
}
pub fn draft(&self) -> NodeDraft {
NodeDraft {
short_name: self.short_name.clone(),
short_description: self.short_description.clone(),
long_description: self.long_description.clone(),
owner: self.owner.clone(),
fixed_connections: self
.fixed_connections
.iter()
.map(|connection| connection.id.clone())
.collect(),
recent_connections: self
.recent_connections
.iter()
.map(|connection| connection.id.clone())
.collect(),
objects: self.objects.clone(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeDraft {
pub short_name: String,
pub short_description: String,
pub long_description: String,
pub owner: String,
#[serde(default)]
pub fixed_connections: Vec<String>,
#[serde(default)]
pub recent_connections: Vec<String>,
#[serde(default)]
pub objects: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StagedCreate {
pub pending_id: String,
pub data: NodeDraft,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BoxKind {
Loaded,
Fixed,
Staged,
Recent,
}
impl BoxKind {
pub const fn name(self) -> &'static str {
match self {
Self::Loaded => "Kweb loaded node",
Self::Fixed => "Kweb fixed connection",
Self::Staged => "Kweb staged node",
Self::Recent => "Kweb recent connections",
}
}
pub const fn metadata_name(self) -> &'static str {
match self {
Self::Loaded => "loaded",
Self::Fixed => "fixed",
Self::Staged => "staged",
Self::Recent => "recent",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BoxSpec {
pub logical_slot: String,
pub kind: BoxKind,
pub text: String,
pub stored_node: Option<Node>,
pub staged_node: Option<NodeDraft>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadReport {
pub requested_id: String,
pub newly_loaded: bool,
pub promoted_from_fixed: bool,
pub new_fixed_ids: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct Context {
root_node_ids: Vec<String>,
loaded_node_ids: Vec<String>,
fixed_node_ids: Vec<String>,
nodes_by_id: BTreeMap<String, Node>,
}
impl Context {
pub fn new(root_node_ids: Vec<String>) -> Result<Self> {
if root_node_ids.is_empty() {
return Err(Error::new("Kweb context requires at least one root node"));
}
let mut seen = HashSet::new();
for id in &root_node_ids {
canonical_node_id(id)?;
if !seen.insert(id.clone()) {
return Err(Error::new("Kweb root node IDs must be distinct"));
}
}
Ok(Self {
root_node_ids,
loaded_node_ids: Vec::new(),
fixed_node_ids: Vec::new(),
nodes_by_id: BTreeMap::new(),
})
}
pub fn root_node_ids(&self) -> &[String] {
&self.root_node_ids
}
pub fn loaded_node_ids(&self) -> &[String] {
&self.loaded_node_ids
}
pub fn fixed_node_ids(&self) -> &[String] {
&self.fixed_node_ids
}
pub fn full_node_ids(&self) -> Vec<&str> {
self.loaded_node_ids
.iter()
.chain(&self.fixed_node_ids)
.map(String::as_str)
.collect()
}
pub fn contains_full_node(&self, id: &str) -> bool {
self.loaded_node_ids.iter().any(|candidate| candidate == id)
|| self.fixed_node_ids.iter().any(|candidate| candidate == id)
}
pub fn node(&self, id: &str) -> Option<&Node> {
self.nodes_by_id.get(id)
}
pub fn apply_load(&mut self, requested: Node, fixed: Vec<Node>) -> Result<LoadReport> {
let requested_id = requested.id.clone();
let was_loaded = self
.loaded_node_ids
.iter()
.any(|candidate| candidate == &requested_id);
let was_fixed = self
.fixed_node_ids
.iter()
.any(|candidate| candidate == &requested_id);
let previous_full = self
.full_node_ids()
.into_iter()
.map(str::to_owned)
.collect::<HashSet<_>>();
let expected_fixed = requested
.fixed_connections
.iter()
.map(|connection| connection.id.as_str())
.filter(|id| *id != requested_id)
.collect::<HashSet<_>>();
let provided_fixed = fixed
.iter()
.map(|node| node.id.as_str())
.collect::<HashSet<_>>();
if expected_fixed != provided_fixed {
return Err(Error::new(format!(
"load for {requested_id} did not provide exactly its fixed connections"
)));
}
self.nodes_by_id.insert(requested_id.clone(), requested);
for node in fixed {
self.nodes_by_id.insert(node.id.clone(), node);
}
if !was_loaded {
self.loaded_node_ids.push(requested_id.clone());
}
self.rebuild_fixed();
let new_fixed_ids = self
.fixed_node_ids
.iter()
.filter(|id| !previous_full.contains(*id))
.cloned()
.collect();
Ok(LoadReport {
requested_id,
newly_loaded: !was_loaded && !was_fixed,
promoted_from_fixed: !was_loaded && was_fixed,
new_fixed_ids,
})
}
pub fn refresh(&mut self, nodes: impl IntoIterator<Item = Node>) -> Result<()> {
for node in nodes {
if !self.contains_full_node(&node.id) {
return Err(Error::new(format!(
"cannot refresh unloaded Kweb node {}",
node.id
)));
}
self.nodes_by_id.insert(node.id.clone(), node);
}
self.rebuild_fixed();
Ok(())
}
pub fn restore(
&mut self,
nodes: impl IntoIterator<Item = Node>,
directly_loaded: Vec<String>,
) -> Result<()> {
self.nodes_by_id.clear();
for node in nodes {
self.nodes_by_id.insert(node.id.clone(), node);
}
let mut seen = HashSet::new();
self.loaded_node_ids = directly_loaded
.into_iter()
.filter(|id| self.nodes_by_id.contains_key(id) && seen.insert(id.clone()))
.collect();
if !self.nodes_by_id.is_empty() && self.loaded_node_ids.is_empty() {
return Err(Error::new(
"restored Kweb context contains nodes but no loaded node",
));
}
self.rebuild_fixed();
Ok(())
}
pub fn box_specs(
&self,
updates: &BTreeMap<String, NodeDraft>,
creates: &[StagedCreate],
) -> Result<Vec<BoxSpec>> {
for id in updates.keys() {
if !self.contains_full_node(id) {
return Err(Error::new(format!(
"staged update targets unloaded Kweb node {id}"
)));
}
}
let mut specs = Vec::new();
for id in &self.loaded_node_ids {
specs.push(self.full_box(id, BoxKind::Loaded, updates.get(id))?);
}
for id in &self.fixed_node_ids {
specs.push(self.full_box(id, BoxKind::Fixed, updates.get(id))?);
}
for create in creates {
specs.push(BoxSpec {
logical_slot: create.pending_id.clone(),
kind: BoxKind::Staged,
text: format_node(&create.pending_id, &create.data),
stored_node: None,
staged_node: Some(create.data.clone()),
});
}
specs.push(BoxSpec {
logical_slot: "recent-connections".into(),
kind: BoxKind::Recent,
text: self.format_recent_connections(updates, creates)?,
stored_node: None,
staged_node: None,
});
Ok(specs)
}
fn full_box(&self, id: &str, kind: BoxKind, update: Option<&NodeDraft>) -> Result<BoxSpec> {
let node = self
.nodes_by_id
.get(id)
.ok_or_else(|| Error::new(format!("missing full Kweb node {id}")))?;
let data = update.cloned().unwrap_or_else(|| node.draft());
Ok(BoxSpec {
logical_slot: id.to_owned(),
kind,
text: format_node(id, &data),
stored_node: Some(node.clone()),
staged_node: update.cloned(),
})
}
fn format_recent_connections(
&self,
updates: &BTreeMap<String, NodeDraft>,
creates: &[StagedCreate],
) -> Result<String> {
let creates_by_id = creates
.iter()
.map(|create| (create.pending_id.as_str(), &create.data))
.collect::<HashMap<_, _>>();
let mut summaries = HashMap::new();
for node in self.nodes_by_id.values() {
summaries.insert(
node.id.as_str(),
(node.short_name.as_str(), node.short_description.as_str()),
);
for connection in node
.fixed_connections
.iter()
.chain(&node.recent_connections)
{
summaries.entry(connection.id.as_str()).or_insert((
connection.short_name.as_str(),
connection.short_description.as_str(),
));
}
}
let mut recent_ids = Vec::new();
let mut seen = HashSet::new();
for id in self.full_node_ids() {
let node = self
.nodes_by_id
.get(id)
.ok_or_else(|| Error::new(format!("missing full Kweb node {id}")))?;
let recent = updates
.get(id)
.map(|draft| draft.recent_connections.as_slice())
.unwrap_or_else(|| &[]);
if updates.contains_key(id) {
for connection_id in recent {
if seen.insert(connection_id.clone()) {
recent_ids.push(connection_id.clone());
}
}
} else {
for connection in &node.recent_connections {
if seen.insert(connection.id.clone()) {
recent_ids.push(connection.id.clone());
}
}
}
}
for create in creates {
for connection_id in &create.data.recent_connections {
if seen.insert(connection_id.clone()) {
recent_ids.push(connection_id.clone());
}
}
}
let mut lines = vec!["Recent connections".to_owned()];
for id in recent_ids {
let staged_summary = updates
.get(&id)
.or_else(|| creates_by_id.get(id.as_str()).copied())
.map(|node| (node.short_name.as_str(), node.short_description.as_str()));
let (name, description) = staged_summary
.or_else(|| summaries.get(id.as_str()).copied())
.ok_or_else(|| {
Error::new(format!(
"recent connection {id} must resolve to a nonempty short name and short description"
))
})?;
if name.trim().is_empty() || description.trim().is_empty() {
return Err(Error::new(format!(
"recent connection {id} must resolve to a nonempty short name and short description"
)));
}
lines.push(format!("{id} · {name}: {description}"));
}
if lines.len() == 1 {
lines.push("None.".into());
}
Ok(lines.join("\n"))
}
fn rebuild_fixed(&mut self) {
let loaded = self.loaded_node_ids.iter().cloned().collect::<HashSet<_>>();
let mut seen = loaded.clone();
let mut fixed = Vec::new();
for id in &self.loaded_node_ids {
let Some(node) = self.nodes_by_id.get(id) else {
continue;
};
for connection in &node.fixed_connections {
if self.nodes_by_id.contains_key(&connection.id)
&& seen.insert(connection.id.clone())
{
fixed.push(connection.id.clone());
}
}
}
self.fixed_node_ids = fixed;
self.nodes_by_id
.retain(|id, _| loaded.contains(id) || seen.contains(id));
}
}
fn connections(
value: Option<&Value>,
summaries: &HashMap<String, &Value>,
label: &str,
) -> Result<Vec<Connection>> {
let mut result = Vec::new();
let mut seen = HashSet::new();
for entry in value.and_then(Value::as_array).into_iter().flatten() {
let id = entry
.as_str()
.or_else(|| entry.get("id").and_then(Value::as_str))
.ok_or_else(|| Error::new(format!("{label} has no node ID")))?
.to_owned();
canonical_node_id(&id)?;
if !seen.insert(id.clone()) {
continue;
}
let summary = summaries.get(&id).copied();
result.push(Connection {
id,
short_name: entry
.get("short_name")
.and_then(Value::as_str)
.or_else(|| summary.and_then(|value| value.get("short_name")?.as_str()))
.unwrap_or_default()
.to_owned(),
short_description: entry
.get("short_description")
.and_then(Value::as_str)
.or_else(|| summary.and_then(|value| value.get("short_description")?.as_str()))
.unwrap_or_default()
.to_owned(),
});
}
Ok(result)
}
fn string_ids(value: Option<&Value>, label: &str) -> Result<Vec<String>> {
let mut result = Vec::new();
let mut seen = HashSet::new();
for entry in value.and_then(Value::as_array).into_iter().flatten() {
let id = entry
.as_str()
.ok_or_else(|| Error::new(format!("{label} ID must be a string")))?
.to_owned();
if seen.insert(id.clone()) {
result.push(id);
}
}
Ok(result)
}
fn canonical_node_id(value: &str) -> Result<()> {
value
.parse::<NodeId>()
.map(|_| ())
.map_err(|_| Error::new(format!("{value:?} is not a canonical Kweb node ID")))
}
fn required_string(value: &Value, key: &str) -> Result<String> {
value
.get(key)
.and_then(Value::as_str)
.map(str::to_owned)
.ok_or_else(|| Error::new(format!("Kweb node has no string {key}")))
}
fn optional_string(value: &Value, key: &str) -> String {
value
.get(key)
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned()
}
fn format_node(identifier: &str, node: &NodeDraft) -> String {
[
format!("Node ID: {identifier}"),
format!("Node name: {}", fallback(&node.short_name)),
format!("Node summary: {}", fallback(&node.short_description)),
format!("Node owner ID: {}", fallback(&node.owner)),
"Node long description:".into(),
indent(&node.long_description),
format!(
"Fixed connection IDs: {}",
list_or_none(&node.fixed_connections)
),
format!(
"Recent connection IDs: {}",
list_or_none(&node.recent_connections)
),
]
.join("\n")
}
fn indent(value: &str) -> String {
fallback(value)
.lines()
.map(|line| format!(" {line}"))
.collect::<Vec<_>>()
.join("\n")
}
fn fallback(value: &str) -> &str {
if value.trim().is_empty() {
"(none)"
} else {
value
}
}
fn list_or_none(values: &[String]) -> String {
if values.is_empty() {
"none".into()
} else {
values.join(", ")
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn id(index: u8) -> String {
NodeId::from_bytes([0, 0, 0, 0, 0, index])
.unwrap()
.to_string()
}
fn connection(index: u8) -> Connection {
Connection {
id: id(index),
short_name: format!("Node {index}"),
short_description: format!("Summary {index}"),
}
}
fn node(index: u8, fixed: &[u8], recent: &[u8]) -> Node {
Node {
id: id(index),
short_name: format!("Node {index}"),
short_description: format!("Summary {index}"),
long_description: format!("Long description {index}"),
owner: id(1),
fixed_connections: fixed.iter().copied().map(connection).collect(),
recent_connections: recent.iter().copied().map(connection).collect(),
objects: vec![],
last_modified_by: "test-model-high".into(),
last_modified_at: Some("2026-07-28T00:00:00Z".into()),
}
}
fn draft(index: u8, recent: &[u8]) -> NodeDraft {
NodeDraft {
short_name: format!("Node {index}"),
short_description: format!("Summary {index}"),
long_description: format!("Long description {index}"),
owner: id(1),
fixed_connections: Vec::new(),
recent_connections: recent.iter().map(|value| id(*value)).collect(),
objects: Vec::new(),
}
}
#[test]
fn parses_the_kweb_wire_shape_into_typed_connections() {
let parsed = Node::from_kweb_value(&json!({
"id": id(1),
"owner_node_id": id(1),
"short_name": "Root",
"short_description": "Root summary",
"long_description": "Root details",
"fixed_connections": [id(2)],
"recent_connections": [id(3), id(3)],
"objects": [],
"connection_summaries": [
{"id":id(2),"short_name":"Fixed","short_description":"Fixed summary"},
{"id":id(3),"short_name":"Recent","short_description":"Recent summary"}
]
}))
.unwrap();
assert_eq!(
parsed.fixed_connections,
vec![Connection {
id: id(2),
short_name: "Fixed".into(),
short_description: "Fixed summary".into(),
}]
);
assert_eq!(parsed.recent_connections.len(), 1);
assert_eq!(parsed.recent_connections[0].short_name, "Recent");
}
#[test]
fn loaded_nodes_take_precedence_over_the_fixed_role() {
let mut context = Context::new(vec![id(1)]).unwrap();
context
.apply_load(node(1, &[2], &[]), vec![node(2, &[], &[])])
.unwrap();
let report = context.apply_load(node(2, &[], &[]), Vec::new()).unwrap();
assert!(report.promoted_from_fixed);
assert_eq!(context.loaded_node_ids(), &[id(1), id(2)]);
assert!(context.fixed_node_ids().is_empty());
assert_eq!(
context
.box_specs(&BTreeMap::new(), &[])
.unwrap()
.iter()
.map(|spec| spec.kind)
.collect::<Vec<_>>(),
vec![BoxKind::Loaded, BoxKind::Loaded, BoxKind::Recent]
);
}
#[test]
fn full_node_kinds_share_one_body_format_without_active_connections() {
let mut context = Context::new(vec![id(1)]).unwrap();
context
.apply_load(node(1, &[2], &[]), vec![node(2, &[], &[])])
.unwrap();
let create = StagedCreate {
pending_id: "pending:1".into(),
data: draft(3, &[]),
};
let specs = context.box_specs(&BTreeMap::new(), &[create]).unwrap();
assert_eq!(specs[0].kind.name(), "Kweb loaded node");
assert_eq!(specs[1].kind.name(), "Kweb fixed connection");
assert_eq!(specs[2].kind.name(), "Kweb staged node");
for spec in &specs[..3] {
assert!(spec.text.contains("Node ID:"));
assert!(spec.text.contains("Node name:"));
assert!(spec.text.contains("Node owner ID:"));
assert!(spec.text.contains("Fixed connection IDs:"));
assert!(spec.text.contains("Recent connection IDs:"));
assert!(!spec.text.contains("Active"));
}
assert_eq!(
specs[0].text,
concat!(
"Node ID: AAAAAAAB\n",
"Node name: Node 1\n",
"Node summary: Summary 1\n",
"Node owner ID: AAAAAAAB\n",
"Node long description:\n",
" Long description 1\n",
"Fixed connection IDs: AAAAAAAC\n",
"Recent connection IDs: none"
)
);
}
#[test]
fn all_recent_connections_share_one_globally_deduplicated_box() {
let mut context = Context::new(vec![id(1)]).unwrap();
context
.apply_load(node(1, &[2], &[4, 5]), vec![node(2, &[], &[5, 6, 7])])
.unwrap();
let creates = vec![StagedCreate {
pending_id: "pending:1".into(),
data: draft(3, &[6, 7]),
}];
let specs = context.box_specs(&BTreeMap::new(), &creates).unwrap();
let recent = specs
.iter()
.filter(|spec| spec.kind == BoxKind::Recent)
.collect::<Vec<_>>();
assert_eq!(recent.len(), 1);
assert_eq!(
recent[0].text,
format!(
concat!(
"Recent connections\n",
"{} · Node 4: Summary 4\n",
"{} · Node 5: Summary 5\n",
"{} · Node 6: Summary 6\n",
"{} · Node 7: Summary 7"
),
id(4),
id(5),
id(6),
id(7)
)
);
}
#[test]
fn empty_recent_projection_is_still_one_exact_box() {
let mut context = Context::new(vec![id(1)]).unwrap();
context.apply_load(node(1, &[], &[]), Vec::new()).unwrap();
let specs = context.box_specs(&BTreeMap::new(), &[]).unwrap();
assert_eq!(specs.len(), 2);
assert_eq!(specs[1].kind, BoxKind::Recent);
assert_eq!(specs[1].text, "Recent connections\nNone.");
}
#[test]
fn recent_projection_rejects_missing_name_or_description() {
for missing_name in [true, false] {
let mut source = node(1, &[], &[2]);
if missing_name {
source.recent_connections[0].short_name.clear();
} else {
source.recent_connections[0].short_description.clear();
}
let mut context = Context::new(vec![id(1)]).unwrap();
context.apply_load(source, Vec::new()).unwrap();
assert_eq!(
context
.box_specs(&BTreeMap::new(), &[])
.unwrap_err()
.to_string(),
format!(
"recent connection {} must resolve to a nonempty short name and short description",
id(2)
)
);
}
}
#[test]
fn staged_updates_drive_full_text_and_recent_projection() {
let mut context = Context::new(vec![id(1)]).unwrap();
context
.apply_load(node(1, &[3], &[2]), vec![node(3, &[], &[])])
.unwrap();
let mut updates = BTreeMap::new();
updates.insert(id(1), draft(9, &[3]));
let specs = context.box_specs(&updates, &[]).unwrap();
assert!(specs[0].text.contains("Node name: Node 9"));
assert!(specs[0].staged_node.is_some());
assert!(!specs.last().unwrap().text.contains(&id(2)));
assert!(specs.last().unwrap().text.contains(&id(3)));
}
}