use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use chappi_syntax::{Document, Form, canonicalize};
use unicode_normalization::UnicodeNormalization;
use crate::{
CognitiveState, Confidence, ConstraintStrength, FocusFrame, GoalStatus, Node, NodeKind,
QuestionStatus, Relation, RelationKind, TensionStatus, TransitionKind, state_hash,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CapsuleBudget {
pub limit: usize,
pub unit: BudgetUnit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BudgetUnit {
Bytes,
Tokens,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReentryCapsule {
pub schema_version: u32,
pub source_state_hash: [u8; 32],
pub nodes: Vec<Node>,
pub relations: Vec<Relation>,
pub focus_cues: Vec<FocusCue>,
pub omitted_node_ids: Vec<String>,
pub used: usize,
pub requested_limit: usize,
pub requested_unit: BudgetUnit,
pub over_budget: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FocusCue {
pub stream_id: String,
pub sequence: u64,
pub owner: String,
pub transition: TransitionKind,
pub foreground: Vec<String>,
pub background: Vec<String>,
pub residue: Vec<String>,
pub trigger: String,
}
pub trait TokenEstimator {
fn estimate(&self, canonical: &str) -> usize;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct LexicalTokenEstimator;
impl TokenEstimator for LexicalTokenEstimator {
fn estimate(&self, canonical: &str) -> usize {
let mut count = 0;
let mut chars = canonical.chars().peekable();
while let Some(character) = chars.next() {
match character {
'(' | ')' => count += 1,
'"' => {
count += 1;
let mut escaped = false;
for next in chars.by_ref() {
if escaped {
escaped = false;
} else if next == '\\' {
escaped = true;
} else if next == '"' {
break;
}
}
}
character if character.is_whitespace() => {}
_ => {
count += 1;
while chars
.peek()
.is_some_and(|next| !next.is_whitespace() && !matches!(next, '(' | ')'))
{
chars.next();
}
}
}
}
count
}
}
pub fn build_capsule(state: &CognitiveState, budget: CapsuleBudget) -> ReentryCapsule {
build_capsule_with_estimator(state, budget, &LexicalTokenEstimator)
}
pub fn build_capsule_with_estimator(
state: &CognitiveState,
budget: CapsuleBudget,
estimator: &dyn TokenEstimator,
) -> ReentryCapsule {
let nodes_by_id = state
.nodes
.iter()
.map(|node| (node.id.as_str(), node))
.collect::<HashMap<_, _>>();
let focus_rank = focus_rank(state);
let mandatory = state
.nodes
.iter()
.filter(|node| is_mandatory(node))
.map(|node| node.id.as_str())
.collect::<Vec<_>>();
let mut selected = HashSet::new();
for id in mandatory {
add_node_closure(id, &nodes_by_id, &mut selected);
}
loop {
let mut candidates = state
.nodes
.iter()
.filter(|node| !selected.contains(node.id.as_str()))
.collect::<Vec<_>>();
candidates.sort_by(|left, right| compare_nodes(left, right, &focus_rank, state, &selected));
let mut accepted = false;
for candidate in candidates {
let mut expanded = selected.clone();
add_node_closure(&candidate.id, &nodes_by_id, &mut expanded);
let used = measure_selection(state, &expanded, budget.unit, estimator, &focus_rank);
if used <= budget.limit {
selected = expanded;
accepted = true;
break;
}
}
if !accepted {
break;
}
}
assemble_capsule(state, selected, budget, estimator, &focus_rank)
}
impl ReentryCapsule {
pub fn canonical_content(&self) -> String {
canonical_content(&self.nodes, &self.relations, &self.focus_cues)
}
}
fn assemble_capsule(
state: &CognitiveState,
selected: HashSet<&str>,
budget: CapsuleBudget,
estimator: &dyn TokenEstimator,
focus_rank: &HashMap<&str, (u8, u64, String)>,
) -> ReentryCapsule {
let mut nodes = state
.nodes
.iter()
.filter(|node| selected.contains(node.id.as_str()))
.cloned()
.collect::<Vec<_>>();
nodes.sort_by(|left, right| compare_nodes(left, right, focus_rank, state, &selected));
let relations = selected_relations(state, &selected);
let focus_cues = selected_focus_cues(state, &selected);
let canonical = canonical_content(&nodes, &relations, &focus_cues);
let used = measure(&canonical, budget.unit, estimator);
let mut omitted_node_ids = state
.nodes
.iter()
.filter(|node| !selected.contains(node.id.as_str()))
.map(|node| node.id.clone())
.collect::<Vec<_>>();
omitted_node_ids.sort();
ReentryCapsule {
schema_version: 1,
source_state_hash: state_hash(state),
nodes,
relations,
focus_cues,
omitted_node_ids,
used,
requested_limit: budget.limit,
requested_unit: budget.unit,
over_budget: used > budget.limit,
}
}
fn is_mandatory(node: &Node) -> bool {
matches!(node.kind, NodeKind::Constraint { .. })
|| matches!(
node.kind,
NodeKind::Goal {
status: GoalStatus::Committed
}
)
}
fn add_node_closure<'a>(
id: &'a str,
nodes_by_id: &HashMap<&'a str, &'a Node>,
selected: &mut HashSet<&'a str>,
) {
if !selected.insert(id) {
return;
}
let Some(node) = nodes_by_id.get(id) else {
return;
};
add_node_closure(&node.owner, nodes_by_id, selected);
match &node.kind {
NodeKind::Relationship { target, .. }
| NodeKind::Affect { target, .. }
| NodeKind::Perspective(crate::Perspective { target, .. }) => {
add_node_closure(target, nodes_by_id, selected);
}
_ => {}
}
}
fn focus_rank(state: &CognitiveState) -> HashMap<&str, (u8, u64, String)> {
let mut ranks = HashMap::new();
for stream in &state.focus_streams {
let Some(frame) = stream.frames.iter().max_by_key(|frame| frame.sequence) else {
continue;
};
if frame.transition == TransitionKind::Resolved {
continue;
}
for (bucket, ids) in [(0, &frame.foreground), (1, &frame.residue)] {
for id in ids {
let rank = (bucket, u64::MAX - frame.sequence, stream.id.clone());
ranks
.entry(id.as_str())
.and_modify(|old| {
if rank < *old {
*old = rank.clone();
}
})
.or_insert(rank);
}
}
}
ranks
}
fn compare_nodes(
left: &Node,
right: &Node,
focus_rank: &HashMap<&str, (u8, u64, String)>,
state: &CognitiveState,
selected: &HashSet<&str>,
) -> Ordering {
node_rank(left, focus_rank, state, selected)
.cmp(&node_rank(right, focus_rank, state, selected))
.then_with(|| left.id.cmp(&right.id))
}
fn node_rank(
node: &Node,
focus_rank: &HashMap<&str, (u8, u64, String)>,
state: &CognitiveState,
selected: &HashSet<&str>,
) -> (u8, u8, u8, u64, String) {
let stable = node.id.clone();
match &node.kind {
NodeKind::Constraint {
strength: ConstraintStrength::Hard,
} => (0, 0, 0, 0, stable),
NodeKind::Constraint {
strength: ConstraintStrength::Soft,
} => (1, 0, 0, 0, stable),
NodeKind::Goal {
status: GoalStatus::Committed,
} => (2, 0, 0, 0, stable),
NodeKind::Tension {
status: TensionStatus::Open,
} => (3, 0, 0, 0, stable),
NodeKind::Question {
status: QuestionStatus::Open,
} => (4, 0, 0, 0, stable),
_ if focus_rank.contains_key(node.id.as_str()) => {
let (bucket, inverse_sequence, stream) = &focus_rank[node.id.as_str()];
(
5,
*bucket,
0,
*inverse_sequence,
format!("{stream}\0{stable}"),
)
}
NodeKind::Relationship { .. } => (6, 0, 0, u64::MAX - node.salience as u64, stable),
NodeKind::Affect { .. } => (7, 0, 0, u64::MAX - node.salience as u64, stable),
NodeKind::Evidence { .. } if supports_selected(node, state, selected) => {
(8, 0, 0, u64::MAX - node.salience as u64, stable)
}
NodeKind::Claim { confidence } => (
9,
confidence_rank(*confidence),
0,
u64::MAX - node.salience as u64,
stable,
),
_ => (10, 0, 0, u64::MAX - node.salience as u64, stable),
}
}
fn confidence_rank(confidence: Confidence) -> u8 {
match confidence {
Confidence::Settled => 0,
Confidence::Likely => 1,
Confidence::Plausible => 2,
Confidence::Tentative => 3,
}
}
fn supports_selected(node: &Node, state: &CognitiveState, selected: &HashSet<&str>) -> bool {
state.relations.iter().any(|relation| {
relation.kind == RelationKind::Supports
&& relation.source == node.id
&& selected.contains(relation.target.as_str())
&& state.nodes.iter().any(|target| {
target.id == relation.target
&& matches!(
target.kind,
NodeKind::Claim { .. } | NodeKind::Prediction { .. }
)
})
})
}
fn measure_selection(
state: &CognitiveState,
selected: &HashSet<&str>,
unit: BudgetUnit,
estimator: &dyn TokenEstimator,
focus_rank: &HashMap<&str, (u8, u64, String)>,
) -> usize {
let mut nodes = state
.nodes
.iter()
.filter(|node| selected.contains(node.id.as_str()))
.cloned()
.collect::<Vec<_>>();
nodes.sort_by(|left, right| compare_nodes(left, right, focus_rank, state, selected));
let relations = selected_relations(state, selected);
let focus = selected_focus_cues(state, selected);
measure(
&canonical_content(&nodes, &relations, &focus),
unit,
estimator,
)
}
fn measure(canonical: &str, unit: BudgetUnit, estimator: &dyn TokenEstimator) -> usize {
match unit {
BudgetUnit::Bytes => canonical.len(),
BudgetUnit::Tokens => estimator.estimate(canonical),
}
}
fn selected_relations(state: &CognitiveState, selected: &HashSet<&str>) -> Vec<Relation> {
let mut relations = state
.relations
.iter()
.filter(|relation| {
selected.contains(relation.source.as_str())
&& selected.contains(relation.target.as_str())
})
.cloned()
.collect::<Vec<_>>();
relations.sort_by(|left, right| {
left.kind
.as_str()
.cmp(right.kind.as_str())
.then_with(|| left.source.cmp(&right.source))
.then_with(|| left.target.cmp(&right.target))
});
relations
}
fn selected_focus_cues(state: &CognitiveState, selected: &HashSet<&str>) -> Vec<FocusCue> {
let mut cues = state
.focus_streams
.iter()
.filter_map(|stream| stream.frames.iter().max_by_key(|frame| frame.sequence))
.filter(|frame| frame.transition != TransitionKind::Resolved)
.filter(|frame| focus_is_closed(frame, selected))
.map(|frame| FocusCue {
stream_id: frame.stream_id.clone(),
sequence: frame.sequence,
owner: frame.owner.clone(),
transition: frame.transition,
foreground: sorted_strings(&frame.foreground),
background: sorted_strings(&frame.background),
residue: sorted_strings(&frame.residue),
trigger: canonical_form(&frame.trigger),
})
.collect::<Vec<_>>();
cues.sort_by(|left, right| {
left.stream_id
.cmp(&right.stream_id)
.then_with(|| left.sequence.cmp(&right.sequence))
});
cues
}
fn focus_is_closed(frame: &FocusFrame, selected: &HashSet<&str>) -> bool {
selected.contains(frame.owner.as_str())
&& frame
.foreground
.iter()
.chain(&frame.background)
.chain(&frame.residue)
.all(|id| selected.contains(id.as_str()))
}
fn sorted_strings(values: &[String]) -> Vec<String> {
let mut values = values.to_vec();
values.sort();
values
}
fn canonical_content(nodes: &[Node], relations: &[Relation], focus: &[FocusCue]) -> String {
let mut output = String::new();
for node in nodes {
write_node(&mut output, node);
}
for relation in relations {
write!(
output,
"(R{}:{}>{})",
short_relation(relation.kind),
atom(&relation.source),
atom(&relation.target)
)
.expect("writing to String cannot fail");
}
for cue in focus {
write_focus(&mut output, cue);
}
output
}
fn write_node(output: &mut String, node: &Node) {
write!(
output,
"({}~{}@{}/{}{}:",
record_leading_node_id(&node.id),
base36(node.revision),
atom(&node.owner),
short_lifetime(node.lifetime),
base36(node.salience.into())
)
.expect("writing to String cannot fail");
write_kind(output, &node.kind);
output.push(' ');
output.push_str(&canonical_form(&node.payload));
output.push(')');
}
fn write_kind(output: &mut String, kind: &NodeKind) {
match kind {
NodeKind::Entity { entity_kind } if entity_kind == "person" => write!(output, "ep"),
NodeKind::Entity { entity_kind } => write!(output, "e/{}", atom(entity_kind)),
NodeKind::Claim { confidence } => {
write!(output, "c{}", short_confidence(*confidence))
}
NodeKind::Evidence {
evidence_kind,
source,
} => write!(
output,
"v/{}/{}/{}",
atom(evidence_kind),
match source {
crate::Source::Symbol(_) => "s",
crate::Source::String(_) => "q",
},
atom(match source {
crate::Source::Symbol(value) | crate::Source::String(value) => value,
})
),
NodeKind::Goal { status } => write!(output, "g{}", short_goal(*status)),
NodeKind::Constraint { strength } => {
write!(output, "k{}", short_constraint(*strength))
}
NodeKind::Question { status } => write!(output, "q{}", short_question(*status)),
NodeKind::Relationship { target, status } => {
write!(output, "r/{}/{}", atom(target), atom(status))
}
NodeKind::Desire { force } => write!(output, "d{}", short_desire(*force)),
NodeKind::Attention { mode } => write!(output, "a{}", short_attention(*mode)),
NodeKind::Association => write!(output, "s"),
NodeKind::Tension { status } => write!(output, "t{}", short_tension(*status)),
NodeKind::Prediction { confidence, status } => write!(
output,
"p{}{}",
short_confidence(*confidence),
short_prediction(*status)
),
NodeKind::Perspective(value) => write!(
output,
"w/{}/{}",
atom(&value.target),
short_confidence(value.confidence)
),
NodeKind::Affect {
target,
affect_kind,
intensity,
} => write!(
output,
"f/{}/{}/{}",
atom(target),
atom(affect_kind),
short_affect(*intensity)
),
NodeKind::Metacognition { status } => {
write!(output, "m{}", short_metacognition(*status))
}
}
.expect("writing to String cannot fail");
}
fn write_focus(output: &mut String, cue: &FocusCue) {
write!(
output,
"(!{}~{}@{}:{}",
atom(&cue.stream_id),
base36(cue.sequence),
atom(&cue.owner),
short_transition(cue.transition)
)
.expect("writing to String cannot fail");
output.push('/');
write_ids(output, "f", &cue.foreground);
output.push('/');
write_ids(output, "b", &cue.background);
output.push('/');
write_ids(output, "r", &cue.residue);
output.push('#');
output.push_str(&cue.trigger);
output.push(')');
}
fn write_ids(output: &mut String, head: &str, ids: &[String]) {
output.push_str(head);
output.push('[');
for (index, id) in ids.iter().enumerate() {
if index > 0 {
output.push(',');
}
output.push_str(&atom(id));
}
output.push(']');
}
fn base36(mut value: u64) -> String {
if value == 0 {
return "0".into();
}
let mut digits = Vec::new();
while value > 0 {
let digit = (value % 36) as u8;
digits.push(if digit < 10 {
(b'0' + digit) as char
} else {
(b'a' + digit - 10) as char
});
value /= 36;
}
digits.iter().rev().collect()
}
fn short_relation(value: crate::RelationKind) -> &'static str {
match value {
crate::RelationKind::Supports => "u",
crate::RelationKind::Contradicts => "x",
crate::RelationKind::Refines => "r",
crate::RelationKind::Causes => "c",
crate::RelationKind::Supersedes => "s",
crate::RelationKind::About => "a",
crate::RelationKind::Blocks => "b",
crate::RelationKind::Associates => "o",
}
}
fn short_transition(value: TransitionKind) -> &'static str {
match value {
TransitionKind::Evoked => "e",
TransitionKind::Associated => "a",
TransitionKind::Interrupted => "i",
TransitionKind::Returned => "r",
TransitionKind::Reframed => "f",
TransitionKind::Resolved => "s",
}
}
pub(crate) fn canonical_form(form: &Form) -> String {
let bytes = canonicalize(&Document {
forms: vec![form.clone()],
});
String::from_utf8(bytes)
.expect("canonical syntax is UTF-8")
.trim_end_matches('\n')
.to_owned()
}
pub(crate) fn hash_hex(hash: [u8; 32]) -> String {
let mut output = String::with_capacity(64);
for byte in hash {
write!(output, "{byte:02x}").expect("writing to String cannot fail");
}
output
}
fn atom(value: &str) -> String {
let value = value.nfc().collect::<String>();
if !value.is_empty()
&& value.chars().all(|character| {
!character.is_control()
&& !matches!(
character,
' ' | '\t'
| '('
| ')'
| '"'
| ';'
| '~'
| '@'
| '/'
| ':'
| ','
| '['
| ']'
| '#'
| '>'
)
})
{
return value;
}
quoted_atom(&value)
}
fn record_leading_node_id(value: &str) -> String {
let value = value.nfc().collect::<String>();
if matches!(value.chars().next(), Some('R' | '!')) {
quoted_atom(&value)
} else {
atom(&value)
}
}
fn quoted_atom(value: &str) -> String {
let mut output = String::from("\"");
for character in value.chars() {
match character {
'"' => output.push_str("\\\""),
'\\' => output.push_str("\\\\"),
'\n' => output.push_str("\\n"),
'\r' => output.push_str("\\r"),
'\t' => output.push_str("\\t"),
character if character.is_control() => {
write!(output, "\\u{{{:x}}}", character as u32)
.expect("writing to String cannot fail");
}
character => output.push(character),
}
}
output.push('"');
output
}
pub(crate) fn lifetime_text(value: crate::Lifetime) -> &'static str {
match value {
crate::Lifetime::Persistent => "persistent",
crate::Lifetime::Working => "working",
crate::Lifetime::Transient => "transient",
}
}
fn short_lifetime(value: crate::Lifetime) -> &'static str {
match value {
crate::Lifetime::Persistent => "p",
crate::Lifetime::Working => "w",
crate::Lifetime::Transient => "t",
}
}
fn short_confidence(value: Confidence) -> &'static str {
match value {
Confidence::Tentative => "t",
Confidence::Plausible => "p",
Confidence::Likely => "l",
Confidence::Settled => "s",
}
}
fn short_goal(value: GoalStatus) -> &'static str {
match value {
GoalStatus::Considering => "n",
GoalStatus::Active => "a",
GoalStatus::Committed => "c",
GoalStatus::Done => "d",
GoalStatus::Dropped => "x",
}
}
fn short_constraint(value: ConstraintStrength) -> &'static str {
match value {
ConstraintStrength::Soft => "s",
ConstraintStrength::Hard => "h",
}
}
fn short_question(value: QuestionStatus) -> &'static str {
match value {
QuestionStatus::Open => "o",
QuestionStatus::Answered => "a",
QuestionStatus::Deferred => "d",
}
}
fn short_tension(value: TensionStatus) -> &'static str {
match value {
TensionStatus::Open => "o",
TensionStatus::Resolved => "r",
}
}
fn short_prediction(value: crate::PredictionStatus) -> &'static str {
match value {
crate::PredictionStatus::Pending => "p",
crate::PredictionStatus::Matched => "m",
crate::PredictionStatus::Surprised => "s",
}
}
fn short_desire(value: crate::DesireForce) -> &'static str {
match value {
crate::DesireForce::Weak => "w",
crate::DesireForce::Moderate => "m",
crate::DesireForce::Strong => "s",
}
}
fn short_attention(value: crate::AttentionMode) -> &'static str {
match value {
crate::AttentionMode::Foreground => "f",
crate::AttentionMode::Background => "b",
}
}
fn short_affect(value: crate::AffectIntensity) -> &'static str {
match value {
crate::AffectIntensity::Weak => "w",
crate::AffectIntensity::Moderate => "m",
crate::AffectIntensity::Strong => "s",
}
}
fn short_metacognition(value: crate::MetacognitionStatus) -> &'static str {
match value {
crate::MetacognitionStatus::Uncertain => "u",
crate::MetacognitionStatus::Monitoring => "m",
crate::MetacognitionStatus::Resolved => "r",
}
}
pub(crate) fn confidence_text(value: Confidence) -> &'static str {
match value {
Confidence::Tentative => "tentative",
Confidence::Plausible => "plausible",
Confidence::Likely => "likely",
Confidence::Settled => "settled",
}
}
pub(crate) fn goal_status(value: GoalStatus) -> &'static str {
match value {
GoalStatus::Considering => "considering",
GoalStatus::Active => "active",
GoalStatus::Committed => "committed",
GoalStatus::Done => "done",
GoalStatus::Dropped => "dropped",
}
}
pub(crate) fn constraint_text(value: ConstraintStrength) -> &'static str {
match value {
ConstraintStrength::Soft => "soft",
ConstraintStrength::Hard => "hard",
}
}
pub(crate) fn question_text(value: QuestionStatus) -> &'static str {
match value {
QuestionStatus::Open => "open",
QuestionStatus::Answered => "answered",
QuestionStatus::Deferred => "deferred",
}
}
pub(crate) fn tension_text(value: TensionStatus) -> &'static str {
match value {
TensionStatus::Open => "open",
TensionStatus::Resolved => "resolved",
}
}
pub(crate) fn prediction_text(value: crate::PredictionStatus) -> &'static str {
match value {
crate::PredictionStatus::Pending => "pending",
crate::PredictionStatus::Matched => "matched",
crate::PredictionStatus::Surprised => "surprised",
}
}
pub(crate) fn desire_text(value: crate::DesireForce) -> &'static str {
match value {
crate::DesireForce::Weak => "weak",
crate::DesireForce::Moderate => "moderate",
crate::DesireForce::Strong => "strong",
}
}
pub(crate) fn attention_text(value: crate::AttentionMode) -> &'static str {
match value {
crate::AttentionMode::Foreground => "foreground",
crate::AttentionMode::Background => "background",
}
}
pub(crate) fn affect_text(value: crate::AffectIntensity) -> &'static str {
match value {
crate::AffectIntensity::Weak => "weak",
crate::AffectIntensity::Moderate => "moderate",
crate::AffectIntensity::Strong => "strong",
}
}
pub(crate) fn metacognition_text(value: crate::MetacognitionStatus) -> &'static str {
match value {
crate::MetacognitionStatus::Uncertain => "uncertain",
crate::MetacognitionStatus::Monitoring => "monitoring",
crate::MetacognitionStatus::Resolved => "resolved",
}
}