#![allow(clippy::empty_line_after_doc_comments)]
use super::args::{EnrichOperation, ReEmbedTarget};
use super::predicates::{entity_description_scan_predicate, *};
use super::scan::sql::{limit_clause, limit_param};
use crate::errors::AppError;
use rusqlite::Connection;
pub(super) fn scan_entities_for_type_validation(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
type_filter: Option<&str>,
) -> Result<Vec<(i64, String, String)>, AppError> {
let limit_sql = limit_clause(3);
let sql = format!(
"SELECT id, name, type FROM entities WHERE namespace = ?1 \
AND (?2 IS NULL OR type = ?2) \
ORDER BY (id * 2654435761) % 2147483647 {limit_sql}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(
rusqlite::params![namespace, type_filter, limit_param(limit)],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
)?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub(super) fn scan_generic_descriptions(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
) -> Result<Vec<(i64, String, String)>, AppError> {
let limit_sql = limit_clause(2);
let generic_pred = generic_description_predicate();
let sql = format!(
"SELECT id, name, description FROM memories WHERE namespace = ?1 AND deleted_at IS NULL \
AND {generic_pred} \
ORDER BY (id * 2654435761) % 2147483647 {limit_sql}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![namespace, limit_param(limit)], |r| {
Ok((r.get(0)?, r.get(1)?, r.get(2)?))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub(super) const DEFAULT_QUALITY_SAMPLE_N: usize = 50;
pub(super) struct DescriptionQualitySample {
pub sampled: u32,
#[allow(dead_code)]
pub grounded: u32,
pub quality_pct: f64,
pub low_grounding_est: i64,
#[allow(dead_code)]
pub total_with_description: i64,
pub without_corpus: u32,
pub percentiles: Option<GroundingPercentiles>,
}
#[derive(Debug, serde::Serialize, schemars::JsonSchema)]
pub(super) struct GroundingPercentiles {
pub p10: f64,
pub p25: f64,
pub p50: f64,
pub p75: f64,
pub p90: f64,
}
fn percentile(sorted: &[f64], q: f64) -> f64 {
if sorted.is_empty() {
return 0.0;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let rank = (q * sorted.len() as f64).ceil() as usize;
sorted[rank.saturating_sub(1).min(sorted.len() - 1)]
}
pub(super) fn sample_entity_description_quality(
conn: &Connection,
namespace: &str,
sample_n: usize,
grounding_threshold: f64,
) -> Result<DescriptionQualitySample, AppError> {
let sample_n = sample_n.max(1);
let threshold = grounding_threshold;
let total_with_description: i64 = conn.query_row(
"SELECT COUNT(*) FROM entities \
WHERE namespace = ?1 \
AND description IS NOT NULL AND description != ''",
rusqlite::params![namespace],
|r| r.get(0),
)?;
let mut stmt = conn.prepare(
"SELECT id, description FROM entities \
WHERE namespace = ?1 \
AND description IS NOT NULL AND description != '' \
ORDER BY (id * 2654435761) % 2147483647 \
LIMIT ?2",
)?;
let rows = stmt.query_map(rusqlite::params![namespace, sample_n as i64], |r| {
Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?))
})?;
let mut without_corpus: u32 = 0;
let mut sampled: u32 = 0;
let mut grounded: u32 = 0;
let capacity = sample_n.min(usize::try_from(total_with_description).unwrap_or(0));
let mut scores: Vec<f64> = Vec::with_capacity(capacity);
let min_corpus_chars = crate::runtime_config::resolve_usize(
None,
"enrich.entity_description.min_corpus_chars",
crate::preservation::DEFAULT_GROUNDING_MIN_CORPUS_CHARS,
);
for row in rows {
let (entity_id, description) = row.map_err(AppError::Database)?;
let corpus = super::extraction::load_entity_evidence(conn, entity_id)?;
sampled = sampled.saturating_add(1);
if !crate::preservation::corpus_is_sufficient(&corpus, min_corpus_chars) {
without_corpus = without_corpus.saturating_add(1);
scores.push(0.0);
continue;
}
let verdict = crate::preservation::PreservationVerdict::evaluate_grounding(
&description,
&corpus,
threshold,
);
if verdict.is_accepted() {
grounded = grounded.saturating_add(1);
}
let score = match verdict {
crate::preservation::PreservationVerdict::Preserved { score, .. } => score,
crate::preservation::PreservationVerdict::Rejected { score, .. } => score,
crate::preservation::PreservationVerdict::Unchanged { .. } => 1.0,
};
scores.push(score);
}
scores.sort_unstable_by(f64::total_cmp);
let percentiles = (!scores.is_empty()).then(|| GroundingPercentiles {
p10: percentile(&scores, 0.10),
p25: percentile(&scores, 0.25),
p50: percentile(&scores, 0.50),
p75: percentile(&scores, 0.75),
p90: percentile(&scores, 0.90),
});
let mean_score = if scores.is_empty() {
1.0
} else {
scores.iter().sum::<f64>() / scores.len() as f64
};
let quality_pct = if sampled == 0 {
1.0
} else {
let accept_rate = f64::from(grounded) / f64::from(sampled);
(crate::constants::ENRICH_QUALITY_ACCEPT_RATE_WEIGHT * accept_rate
+ crate::constants::ENRICH_QUALITY_MEAN_SCORE_WEIGHT * mean_score)
.clamp(0.0, 1.0)
};
let low_frac = (1.0 - quality_pct).clamp(0.0, 1.0);
let low_grounding_est = (low_frac * total_with_description as f64).round() as i64;
Ok(DescriptionQualitySample {
percentiles,
sampled,
grounded,
quality_pct,
low_grounding_est,
total_with_description,
without_corpus,
})
}
pub(super) fn count_operation_backlog(
conn: &Connection,
operation: &EnrichOperation,
namespace: &str,
reembed_target: ReEmbedTarget,
) -> Result<i64, AppError> {
count_operation_backlog_with_force(conn, operation, namespace, reembed_target, false, None)
}
pub(super) fn count_operation_backlog_with_force(
conn: &Connection,
operation: &EnrichOperation,
namespace: &str,
reembed_target: ReEmbedTarget,
force_redescribe: bool,
type_filter: Option<&str>,
) -> Result<i64, AppError> {
let count = match operation {
EnrichOperation::MemoryBindings => {
let sql = format!(
"SELECT COUNT(*) FROM memories m \
WHERE m.namespace = ?1 AND m.deleted_at IS NULL \
AND {UNBOUND_MEMORY_PREDICATE}"
);
conn.query_row(&sql, rusqlite::params![namespace], |r| r.get::<_, i64>(0))?
}
EnrichOperation::EntityDescriptions => {
if force_redescribe {
let pred = entity_description_scan_predicate(true, false);
let sql = format!(
"SELECT COALESCE(description, '') FROM entities \
WHERE namespace = ?1 AND {pred}"
);
let mut stmt = conn.prepare(&sql)?;
let descs =
stmt.query_map(rusqlite::params![namespace], |r| r.get::<_, String>(0))?;
let mut n = 0i64;
for d in descs {
let desc = d?;
if desc.trim().is_empty() || is_low_quality_description(&desc) {
n += 1;
}
}
n
} else {
let pred = entity_description_scan_predicate(false, false);
let sql = format!(
"SELECT COUNT(*) FROM entities \
WHERE namespace = ?1 AND {pred}"
);
conn.query_row(&sql, rusqlite::params![namespace], |r| r.get::<_, i64>(0))?
}
}
EnrichOperation::BodyEnrich => {
let sql = format!(
"SELECT COUNT(*) FROM memories m \
WHERE m.namespace = ?1 AND m.deleted_at IS NULL \
AND {SHORT_BODY_PREDICATE}"
);
let min_chars = super::DEFAULT_BODY_ENRICH_MIN_CHARS as i64;
conn.query_row(&sql, rusqlite::params![namespace, min_chars], |r| {
r.get::<_, i64>(0)
})?
}
EnrichOperation::ReEmbed => {
let dim = crate::constants::embedding_dim();
let mut total = 0i64;
if matches!(reembed_target, ReEmbedTarget::Memories | ReEmbedTarget::All) {
let sql = format!(
"SELECT COUNT(*) FROM memories m \
WHERE m.namespace = ?1 AND m.deleted_at IS NULL \
AND {}",
reembed_memory_predicate(dim)
);
total +=
conn.query_row(&sql, rusqlite::params![namespace], |r| r.get::<_, i64>(0))?;
}
if matches!(reembed_target, ReEmbedTarget::Entities | ReEmbedTarget::All) {
let sql = format!(
"SELECT COUNT(*) FROM entities e WHERE e.namespace = ?1 AND {}",
reembed_entity_predicate(dim)
);
total +=
conn.query_row(&sql, rusqlite::params![namespace], |r| r.get::<_, i64>(0))?;
}
if matches!(reembed_target, ReEmbedTarget::Chunks | ReEmbedTarget::All) {
let sql = format!(
"SELECT COUNT(*) FROM memory_chunks c \
LEFT JOIN memories m ON m.id = c.memory_id \
WHERE (m.namespace = ?1 OR m.id IS NULL) \
AND {}",
reembed_chunk_predicate(dim)
);
total +=
conn.query_row(&sql, rusqlite::params![namespace], |r| r.get::<_, i64>(0))?;
}
total
}
EnrichOperation::WeightCalibrate => {
let high_weight_pred = high_weight_predicate();
let sql = format!(
"SELECT COUNT(*) FROM relationships r \
JOIN entities e1 ON e1.id = r.source_id \
WHERE {high_weight_pred} AND e1.namespace = ?1"
);
conn.query_row(&sql, rusqlite::params![namespace], |r| r.get::<_, i64>(0))?
}
EnrichOperation::RelationReclassify => {
let generic_pred = generic_relation_predicate();
let sql = format!(
"SELECT COUNT(*) FROM relationships r \
JOIN entities e1 ON e1.id = r.source_id \
WHERE {generic_pred} AND e1.namespace = ?1"
);
conn.query_row(&sql, rusqlite::params![namespace], |r| r.get::<_, i64>(0))?
}
EnrichOperation::EntityTypeValidate => {
conn.query_row(
"SELECT COUNT(*) FROM entities WHERE namespace = ?1 \
AND (?2 IS NULL OR type = ?2)",
rusqlite::params![namespace, type_filter],
|r| r.get::<_, i64>(0),
)?
}
EnrichOperation::DescriptionEnrich => {
let generic_pred = generic_description_predicate();
let sql = format!(
"SELECT COUNT(*) FROM memories \
WHERE namespace = ?1 AND deleted_at IS NULL \
AND {generic_pred}"
);
conn.query_row(&sql, rusqlite::params![namespace], |r| r.get::<_, i64>(0))?
}
EnrichOperation::EntityConnect => {
let sql = "SELECT COUNT(*) FROM entities e \
WHERE e.namespace = ?1 AND e.degree = 0 \
AND EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)";
conn.query_row(sql, rusqlite::params![namespace], |r| r.get::<_, i64>(0))?
}
EnrichOperation::AugmentBindings
| EnrichOperation::CrossDomainBridges
| EnrichOperation::DomainClassify
| EnrichOperation::GraphAudit
| EnrichOperation::DeepResearchSynth
| EnrichOperation::BodyExtract => 0,
};
Ok(count)
}
#[cfg(test)]
mod tests {
#[test]
fn quality_blend_weights_sum_to_one() {
let sum = crate::constants::ENRICH_QUALITY_ACCEPT_RATE_WEIGHT
+ crate::constants::ENRICH_QUALITY_MEAN_SCORE_WEIGHT;
assert!(
(sum - 1.0).abs() < f64::EPSILON,
"quality blend weights must sum to 1.0, got {sum}"
);
}
}