use crate::context::ContextCapsule;
const ORDERING_MARKERS: &[&str] = &[
"before",
"after",
"first",
"last",
"latest",
"earliest",
"earlier",
"later",
"order",
"ordering",
"sequence",
"chronological",
"chronologically",
"timeline",
"when",
"then",
"initially",
"originally",
"eventually",
"previously",
"subsequently",
"recent",
"recently",
"since",
"until",
"history",
];
pub fn is_ordering_query(query: &str) -> bool {
let lower = query.to_ascii_lowercase();
lower
.split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.any(|word| ORDERING_MARKERS.contains(&word))
}
fn date_of(created_at: &str) -> &str {
created_at.split('T').next().unwrap_or(created_at)
}
fn dated_summary(summary: &str, date: &str) -> String {
match summary.split_once(" - ") {
Some((prefix, text)) => format!("{prefix} - [{date}] {text}"),
None => format!("[{date}] {summary}"),
}
}
pub fn render_chronologically(
capsules: Vec<ContextCapsule>,
created_at: &std::collections::HashMap<String, String>,
) -> Vec<ContextCapsule> {
let mut dated: Vec<(String, ContextCapsule)> = Vec::new();
let mut undated: Vec<ContextCapsule> = Vec::new();
for capsule in capsules {
match created_at.get(&capsule.expansion_handle) {
Some(ts) => dated.push((ts.clone(), capsule)),
None => undated.push(capsule),
}
}
dated.sort_by(|a, b| a.0.cmp(&b.0));
let mut out: Vec<ContextCapsule> = dated
.into_iter()
.map(|(ts, mut capsule)| {
capsule.summary = dated_summary(&capsule.summary, date_of(&ts));
capsule.token_estimate = capsule.token_estimate.saturating_add(4);
capsule
})
.collect();
out.append(&mut undated);
out
}
pub const CHRONOLOGICAL_NOTE: &str =
"These memories are in chronological order, oldest first, with the date each was recorded.";
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn capsule(handle: &str, summary: &str) -> ContextCapsule {
ContextCapsule {
id: String::new(),
kind: "memory".to_string(),
summary: summary.to_string(),
token_estimate: 10,
expansion_handle: handle.to_string(),
provenance: Vec::new(),
confidence: 0.9,
freshness: 0.5,
relevance: 0.0,
scope_weight: 0.9,
score: 0.5,
superseded_hint: false,
rerank_policy_tier: 0,
claim_revision: None,
facts: vec![],
rerank_usefulness: None,
rerank_trust: None,
}
}
fn handles(capsules: &[ContextCapsule]) -> Vec<&str> {
capsules
.iter()
.map(|c| c.expansion_handle.as_str())
.collect()
}
#[test]
fn ordering_questions_are_recognised() {
for query in [
"did we switch to thiserror before or after the migration",
"what came first, the parser or the lexer",
"when did we adopt edition 2024",
"show me the timeline of schema changes",
"what did we do most recently",
] {
assert!(is_ordering_query(query), "should be ordering: {query:?}");
}
}
#[test]
fn ordinary_questions_are_not_ordering_questions() {
for query in [
"how do I checkpoint the wal",
"add error handling to the parser",
"why does the build fail",
"what is the schema version",
] {
assert!(
!is_ordering_query(query),
"should not be ordering: {query:?}"
);
}
}
#[test]
fn markers_match_whole_words_only() {
assert!(!is_ordering_query("this was an afterthought"));
assert!(!is_ordering_query("refactor the ordering_service module"));
assert!(is_ordering_query("refactor the ordering service"));
}
#[test]
fn capsules_are_reordered_by_time_and_dated() {
let mut created = HashMap::new();
created.insert("memory:b".to_string(), "2026-01-15T10:00:00Z".to_string());
created.insert("memory:a".to_string(), "2026-06-01T09:00:00Z".to_string());
let ordered = render_chronologically(
vec![
capsule("memory:a", "project:fact - switched to thiserror"),
capsule("memory:b", "project:fact - migrated the schema"),
],
&created,
);
assert_eq!(
handles(&ordered),
vec!["memory:b", "memory:a"],
"oldest first"
);
assert_eq!(
ordered[0].summary,
"project:fact - [2026-01-15] migrated the schema"
);
assert_eq!(
ordered[1].summary,
"project:fact - [2026-06-01] switched to thiserror"
);
}
#[test]
fn the_date_survives_the_hooks_summary_stripping() {
let mut created = HashMap::new();
created.insert("memory:a".to_string(), "2026-01-15T10:00:00Z".to_string());
let ordered = render_chronologically(
vec![capsule("memory:a", "project:fact - switched to thiserror")],
&created,
);
let shown = ordered[0]
.summary
.split(" - ")
.nth(1)
.expect("hook renders the text half");
assert!(shown.starts_with("[2026-01-15] "), "got: {shown}");
}
#[test]
fn a_prefixless_summary_is_dated_at_the_front() {
let mut created = HashMap::new();
created.insert("memory:a".to_string(), "2026-01-15T10:00:00Z".to_string());
let ordered = render_chronologically(vec![capsule("memory:a", "bare text")], &created);
assert_eq!(ordered[0].summary, "[2026-01-15] bare text");
}
#[test]
fn undated_capsules_are_kept_after_the_timeline() {
let mut created = HashMap::new();
created.insert("memory:a".to_string(), "2026-01-01T00:00:00Z".to_string());
let ordered = render_chronologically(
vec![
capsule("repo_file:src/main.rs", "repo_file:src/main.rs - fn main"),
capsule("memory:a", "project:fact - a thing"),
],
&created,
);
assert_eq!(
handles(&ordered),
vec!["memory:a", "repo_file:src/main.rs"],
"dated first, undated kept: {:?}",
handles(&ordered)
);
assert!(
!ordered[1].summary.contains('['),
"an undated capsule must not be given a date: {}",
ordered[1].summary
);
}
#[test]
fn equal_timestamps_keep_the_brokers_order() {
let mut created = HashMap::new();
created.insert("memory:a".to_string(), "2026-01-01T00:00:00Z".to_string());
created.insert("memory:b".to_string(), "2026-01-01T00:00:00Z".to_string());
let ordered = render_chronologically(
vec![
capsule("memory:a", "project:fact - best match"),
capsule("memory:b", "project:fact - second"),
],
&created,
);
assert_eq!(handles(&ordered), vec!["memory:a", "memory:b"]);
}
#[test]
fn reordering_never_adds_or_drops_a_capsule() {
let mut created = HashMap::new();
created.insert("memory:a".to_string(), "2026-03-01T00:00:00Z".to_string());
let input = vec![
capsule("memory:a", "a"),
capsule("memory:b", "b"),
capsule("repo_file:x", "x"),
];
let ordered = render_chronologically(input.clone(), &created);
assert_eq!(ordered.len(), input.len());
let mut got = handles(&ordered);
got.sort_unstable();
let mut want = handles(&input);
want.sort_unstable();
assert_eq!(got, want);
}
#[test]
fn the_token_estimate_accounts_for_the_date_prefix() {
let mut created = HashMap::new();
created.insert("memory:a".to_string(), "2026-03-01T00:00:00Z".to_string());
let ordered = render_chronologically(vec![capsule("memory:a", "a")], &created);
assert!(ordered[0].token_estimate > 10, "the prefix costs tokens");
}
}