use super::*;
pub(crate) const VECTOR_B: &str =
"\n\n[mati] Before reading any file: call mem_get(\"file:<path>\").\n\
confidence>=0.6 + confirmed=true \u{2192} use record, skip file read.\n\
confidence<0.3 \u{2192} read file, consider mem_set to improve.\n\
\"add gotcha\" \u{2192} mem_set(Gotcha) then mati gotcha confirm <key>.";
pub(super) const TOKEN_BUDGET: usize = 2_000;
const VECTOR_B_TOKENS: usize = 77;
pub(super) fn estimate_tokens(text: &str) -> usize {
text.len() / 4
}
fn priority_weight(priority: &Priority) -> f32 {
match priority {
Priority::Low => 0.25,
Priority::Normal => 0.50,
Priority::High => 0.75,
Priority::Critical => 1.00,
}
}
pub(crate) fn record_to_agent_json(record: &Record) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert("key".into(), serde_json::json!(record.key));
obj.insert("value".into(), serde_json::json!(record.value));
obj.insert("category".into(), serde_json::json!(record.category));
obj.insert("priority".into(), serde_json::json!(record.priority));
if !record.tags.is_empty() {
obj.insert("tags".into(), serde_json::json!(record.tags));
}
obj.insert(
"confidence".into(),
serde_json::json!(record.confidence.value),
);
obj.insert(
"confirmation_count".into(),
serde_json::json!(record.confidence.confirmation_count),
);
obj.insert("quality".into(), serde_json::json!(record.quality.value));
obj.insert(
"quality_tier".into(),
serde_json::json!(record.quality.tier),
);
if !record.quality.signals.is_empty() {
obj.insert(
"quality_signals".into(),
serde_json::json!(record.quality.signals),
);
}
obj.insert("source".into(), serde_json::json!(record.source));
obj.insert(
"staleness_tier".into(),
serde_json::json!(record.staleness.tier),
);
if let Some(ref url) = record.ref_url {
obj.insert("ref_url".into(), serde_json::json!(url));
}
if let Some(ref payload) = record.payload {
obj.insert("payload".into(), strip_payload(payload, &record.category));
}
serde_json::Value::Object(obj)
}
fn strip_payload(payload: &serde_json::Value, category: &Category) -> serde_json::Value {
let Some(obj) = payload.as_object() else {
return payload.clone();
};
let internal_fields: &[&str] = match category {
Category::File => &[
"token_cost_estimate",
"last_modified_session",
"content_hash",
],
Category::Gotcha => &["discovered_session"],
_ => &[],
};
if internal_fields.is_empty() {
return payload.clone();
}
let mut stripped = obj.clone();
for field in internal_fields {
stripped.remove(*field);
}
if matches!(category, Category::File) {
stripped.retain(|_, v| !matches!(v, serde_json::Value::Array(a) if a.is_empty()));
}
serde_json::Value::Object(stripped)
}
pub(crate) fn is_injectable_gotcha(r: &Record) -> bool {
if !matches!(r.lifecycle, RecordLifecycle::Active) {
return false;
}
if r.staleness.tier == StalenessTier::Tombstone {
return false;
}
if r.quality.value < 0.4 {
return false;
}
if let Some(gotcha) = r.payload_as::<GotchaRecord>() {
if gotcha.confirmed {
return true;
}
}
crate::store::gotcha_ops::is_auto_gotcha(&r.key)
}
pub async fn assemble_context_packet(
store: &crate::store::Store,
graph: &Graph,
context_files: &[String],
) -> anyhow::Result<ContextPacket> {
let stage = store.get("stage:current").await?;
let mut file_records = Vec::new();
let mut context_gotcha_keys = HashSet::new();
let mut decision_keys = HashSet::new();
let mut unconfirmed_candidates = Vec::new();
let mut stale_warnings: Vec<String> = Vec::new();
let mut seen_stale_keys: HashSet<String> = HashSet::new();
for file_path in context_files {
let file_key = if file_path.starts_with("file:") {
file_path.clone()
} else {
format!("file:{file_path}")
};
if let Ok(Some(record)) = store.get(&file_key).await {
if record.staleness.tier == StalenessTier::Tombstone
|| !matches!(record.lifecycle, RecordLifecycle::Active)
{
continue;
}
match record.staleness.tier {
StalenessTier::Stale => {
let path = file_key.strip_prefix("file:").unwrap_or(&file_key);
if seen_stale_keys.insert(file_key.clone()) {
stale_warnings.push(format!(
"`{path}` record is stale (staleness {:.2}) — verify before trusting",
record.staleness.value
));
}
}
StalenessTier::Liability => {
let path = file_key.strip_prefix("file:").unwrap_or(&file_key);
if seen_stale_keys.insert(file_key.clone()) {
stale_warnings.push(format!(
"`{path}` record is a liability (staleness {:.2}) — do not trust, read the file",
record.staleness.value
));
}
}
_ => {}
}
if let Some(fr) = record.payload_as::<FileRecord>() {
for key in &fr.gotcha_keys {
context_gotcha_keys.insert(key.clone());
}
let is_nudge_candidate = record.access_count >= 3 && fr.gotcha_keys.is_empty();
file_records.push(fr);
if is_nudge_candidate {
unconfirmed_candidates.push(file_key.clone());
}
}
}
for key in graph.neighbors(&file_key, &EdgeKind::HasGotcha) {
context_gotcha_keys.insert(key);
}
for imported in graph.neighbors(&file_key, &EdgeKind::Imports) {
for key in graph.neighbors(&imported, &EdgeKind::HasGotcha) {
context_gotcha_keys.insert(key);
}
}
for key in graph.neighbors(&file_key, &EdgeKind::AffectedBy) {
decision_keys.insert(key);
}
}
let mut confirmed_gotchas: Vec<Record> = if context_files.is_empty() {
let all_gotchas = store.scan_prefix("gotcha:").await?;
all_gotchas
.into_iter()
.filter(is_injectable_gotcha)
.collect()
} else {
let mut gotchas = Vec::with_capacity(context_gotcha_keys.len());
for key in &context_gotcha_keys {
if let Ok(Some(record)) = store.get(key).await {
if is_injectable_gotcha(&record) {
gotchas.push(record);
}
}
}
gotchas
};
{
let now = chrono::Utc::now();
for days_ago in 0..2 {
let date = (now - chrono::Duration::days(days_ago)).format("%Y-%m-%d");
let review_key = format!("analytics:stale_review_{date}");
if let Ok(Some(record)) = store.get(&review_key).await {
if let Some(payload) = record.payload_as::<StaleReviewPayload>() {
for entry in &payload.entries {
if seen_stale_keys.insert(entry.key.clone()) {
let path = entry.key.strip_prefix("file:").unwrap_or(&entry.key);
stale_warnings.push(format!(
"`{path}` staleness {:.2} ({:?}) — review recommended",
entry.staleness_value, entry.tier
));
}
}
}
}
}
}
let mut related_decisions = Vec::new();
for key in &decision_keys {
if let Ok(Some(record)) = store.get(key).await {
related_decisions.push(record);
}
}
if related_decisions.is_empty() {
if let Ok(mut all_decisions) = store.scan_prefix("decision:").await {
all_decisions.retain(|r| matches!(r.lifecycle, RecordLifecycle::Active));
all_decisions.sort_by(|a, b| {
b.confidence
.value
.partial_cmp(&a.confidence.value)
.unwrap_or(std::cmp::Ordering::Equal)
});
const DECISION_FALLBACK_LIMIT: usize = 5;
related_decisions = all_decisions
.into_iter()
.take(DECISION_FALLBACK_LIMIT)
.collect();
}
}
confirmed_gotchas.sort_by(|a, b| {
let score_a = a.confidence.value * priority_weight(&a.priority);
let score_b = b.confidence.value * priority_weight(&b.priority);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
let critical_gotchas: Vec<Record> = confirmed_gotchas
.into_iter()
.filter(|r| r.quality.tier != QualityTier::Suppressed)
.collect();
let available_tokens = TOKEN_BUDGET - VECTOR_B_TOKENS;
let mut sections = Vec::new();
let mut used_tokens = 0;
if let Some(ref stage_record) = stage {
let section = format!("## Current Stage\n{}\n", stage_record.value);
let tokens = estimate_tokens(§ion);
if used_tokens + tokens <= available_tokens {
sections.push(section);
used_tokens += tokens;
}
}
if !critical_gotchas.is_empty() {
let mut gotcha_section = String::from("## Gotchas\n");
for record in &critical_gotchas {
if record.key.starts_with("gotcha:cochange:") {
continue;
}
let caveat = if record.staleness.tier == StalenessTier::Liability {
" [STALE — verify]"
} else if record.quality.tier == QualityTier::Poor {
" [LOW QUALITY — verify]"
} else {
""
};
let line = format!("- **{}**{}: {}\n", record.key, caveat, record.value);
let tokens = estimate_tokens(&line);
if used_tokens + tokens > available_tokens {
break;
}
gotcha_section.push_str(&line);
used_tokens += tokens;
}
let mut cochange_map: std::collections::BTreeMap<String, Vec<(String, String)>> =
std::collections::BTreeMap::new();
for record in &critical_gotchas {
if !record.key.starts_with("gotcha:cochange:") {
continue;
}
if let Some(pair) = record.key.strip_prefix("gotcha:cochange:") {
if let Some((src, tgt)) = pair.split_once('|') {
let pct = record
.value
.rfind('(')
.and_then(|i| {
record.value[i + 1..]
.find(')')
.map(|j| &record.value[i + 1..i + 1 + j])
})
.unwrap_or("?");
cochange_map
.entry(src.to_string())
.or_default()
.push((tgt.to_string(), pct.to_string()));
}
}
}
if !cochange_map.is_empty() {
let all_pairs: Vec<String> = cochange_map
.iter()
.flat_map(|(src, targets)| {
targets
.iter()
.map(move |(tgt, pct)| format!("{src}\u{2194}{tgt} ({pct})"))
})
.collect();
let total = all_pairs.len();
let display: Vec<&str> = all_pairs.iter().take(10).map(|s| s.as_str()).collect();
let suffix = if total > 10 {
format!(", +{} more", total - 10)
} else {
String::new()
};
let line = format!("- **Co-change partners**: {}{suffix}\n", display.join(", "));
let tokens = estimate_tokens(&line);
if used_tokens + tokens <= available_tokens {
gotcha_section.push_str(&line);
used_tokens += tokens;
}
}
if gotcha_section.len() > "## Gotchas\n".len() {
sections.push(gotcha_section);
}
}
if !file_records.is_empty() {
let mut file_section = String::from("## Context Files\n");
for fr in &file_records {
if fr.purpose.is_empty() {
continue;
}
let line = format!("- **{}**: {}\n", fr.path, fr.purpose);
let tokens = estimate_tokens(&line);
if used_tokens + tokens > available_tokens {
break;
}
file_section.push_str(&line);
used_tokens += tokens;
}
if file_section.len() > "## Context Files\n".len() {
sections.push(file_section);
}
}
{
use crate::analysis::blast_radius::BlastTier;
let mut impact_files: Vec<(&FileRecord, f32)> = file_records
.iter()
.filter_map(|fr| {
fr.blast_radius.as_ref().and_then(|br| {
if br.tier == BlastTier::Isolated {
None
} else {
Some((fr, br.score))
}
})
})
.collect();
impact_files.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
if !impact_files.is_empty() {
let mut impact_section = String::from("## Highest Impact Files\n");
for (fr, _score) in impact_files.iter().take(3) {
let br = fr
.blast_radius
.as_ref()
.expect("filter_map above kept only files with Some(blast_radius)");
let line = format!(
"- `{}`: {} direct importers ({})\n",
fr.path,
br.direct,
br.tier.label(),
);
let tokens = estimate_tokens(&line);
if used_tokens + tokens > available_tokens {
break;
}
impact_section.push_str(&line);
used_tokens += tokens;
}
if impact_section.len() > "## Highest Impact Files\n".len() {
sections.push(impact_section);
}
}
}
if !stale_warnings.is_empty() {
let mut stale_section = String::from("## Stale Warnings\n");
for warning in &stale_warnings {
let line = format!("- {warning}\n");
let tokens = estimate_tokens(&line);
if used_tokens + tokens > available_tokens {
break;
}
stale_section.push_str(&line);
used_tokens += tokens;
}
if stale_section.len() > "## Stale Warnings\n".len() {
sections.push(stale_section);
}
}
if !related_decisions.is_empty() {
let mut dec_section = String::from("## Decisions\n");
for record in &related_decisions {
let line = format!("- **{}**: {}\n", record.key, record.value);
let tokens = estimate_tokens(&line);
if used_tokens + tokens > available_tokens {
break;
}
dec_section.push_str(&line);
used_tokens += tokens;
}
if dec_section.len() > "## Decisions\n".len() {
sections.push(dec_section);
}
}
let recent_session = store
.get(crate::store::session::SUBAGENT_SUMMARY_KEY)
.await
.ok()
.flatten()
.map(|record| record.value)
.filter(|summary| !summary.trim().is_empty());
if let Some(summary) = &recent_session {
let section = format!("## Recent Subagent\n{summary}\n");
let tokens = estimate_tokens(§ion);
if used_tokens + tokens <= available_tokens {
sections.push(section);
used_tokens += tokens;
}
}
if !unconfirmed_candidates.is_empty() {
let mut nudge_section = String::from("## Suggested Actions\n");
for key in &unconfirmed_candidates {
let path = key.strip_prefix("file:").unwrap_or(key);
let line = format!(
"- `{path}` is read frequently but has no recorded gotchas. The developer may want to run `mati gotcha add {path}`.\n"
);
let tokens = estimate_tokens(&line);
if used_tokens + tokens > available_tokens {
break;
}
nudge_section.push_str(&line);
used_tokens += tokens;
}
if nudge_section.len() > "## Suggested Actions\n".len() {
sections.push(nudge_section);
}
}
let mut injection_string = sections.join("\n");
injection_string.push_str(VECTOR_B);
let token_estimate = estimate_tokens(&injection_string) as u32;
Ok(ContextPacket {
stage,
critical_gotchas,
file_records,
related_decisions,
recent_session,
token_estimate,
stale_warnings,
unconfirmed_candidates,
knowledge_gaps: vec![],
compliance_rate: None,
injection_string,
})
}