新增: 知识路标投影进系统提示词
- published 且验证或高复用条目按项目优先进投影稳定段 - 段尾固定引导工具故障先检索路标,治有经验不想查 - 与规则文件投影共用会话级缓存,空库零回归
This commit is contained in:
@@ -1178,6 +1178,74 @@ impl KnowledgeRepo {
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 知识路标读侧(devflow.md 投影通道):已发布且(人工验证 或 复用达标)的条目选摘。
|
||||
///
|
||||
/// 与检索族(search/search_keywords)的差别:路标无查询词,是「库里有什么」的静态选摘,
|
||||
/// 供 system prompt 路标段铺开让 agent 知晓库藏、想起检索。入选门槛 = published 且
|
||||
/// (verified = 1 OR reuse_count >= 3);排序 = 复用降序(唯一客观信号,对齐 search/top_used),
|
||||
/// 同复用按 created_at 降序定稳定次序。verified 只作入选不作排序(复用是更强的实战信号)。
|
||||
///
|
||||
/// 两档拼接:项目档(source_project = project_id,上限 project_limit)在前——会话归属
|
||||
/// 项目的知识最相关;跨项目补充档(非本项目含无归属条目,上限 global_limit)在后。
|
||||
/// 两档 WHERE 互斥(= pid vs IS NULL/≠ pid),结果天然无重复。
|
||||
/// project_id = None → 仅补充档(会话未解析出归属项目,项目档无从谈起)。
|
||||
pub async fn list_waymark(
|
||||
&self,
|
||||
project_id: Option<&str>,
|
||||
project_limit: usize,
|
||||
global_limit: usize,
|
||||
) -> Result<Vec<KnowledgeRecord>> {
|
||||
let conn = self.conn.clone();
|
||||
let project_id = project_id.map(|s| s.to_owned());
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
// 入选门槛与排序两档共用(verified 在 SQLite 存 0/1;复用达标压线 3 即入选)
|
||||
let qualify = "status = 'published' AND (verified = 1 OR reuse_count >= 3)";
|
||||
let order = " ORDER BY reuse_count DESC, created_at DESC LIMIT ";
|
||||
let mut results = Vec::new();
|
||||
|
||||
// 项目档:会话归属项目内的合格条目(复用降序,上限 project_limit)
|
||||
if let Some(pid) = &project_id {
|
||||
let sql = format!(
|
||||
"SELECT {KNOWLEDGE_COLS} FROM knowledges WHERE {qualify} AND source_project = ?1{order}?2"
|
||||
);
|
||||
let mut stmt = guard.prepare(&sql).map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map(params![pid, project_limit as i64], knowledge_from_row)
|
||||
.map_err(storage_err)?;
|
||||
for r in rows {
|
||||
results.push(r.map_err(storage_err)?);
|
||||
}
|
||||
}
|
||||
|
||||
// 跨项目补充档:非本项目(含无归属全局条目)按复用降序补足 global_limit;
|
||||
// WHERE 与项目档互斥 → 天然去重。project_id = None 时无排他条件,即全量合格条目。
|
||||
// LIMIT 占位符编号随排他条件有无而变(?1 空档排他时归 LIMIT,有排他时排他占 ?1、LIMIT 占 ?2)
|
||||
let exclude = if project_id.is_some() {
|
||||
" AND (source_project IS NULL OR source_project != ?1)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let lim_n = if project_id.is_some() { 2 } else { 1 };
|
||||
let sql = format!("SELECT {KNOWLEDGE_COLS} FROM knowledges WHERE {qualify}{exclude}{order}?{lim_n}");
|
||||
let mut stmt = guard.prepare(&sql).map_err(storage_err)?;
|
||||
let rows = match &project_id {
|
||||
Some(pid) => stmt
|
||||
.query_map(params![pid, global_limit as i64], knowledge_from_row)
|
||||
.map_err(storage_err)?,
|
||||
None => stmt
|
||||
.query_map(params![global_limit as i64], knowledge_from_row)
|
||||
.map_err(storage_err)?,
|
||||
};
|
||||
for r in rows {
|
||||
results.push(r.map_err(storage_err)?);
|
||||
}
|
||||
Ok(results)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
}
|
||||
|
||||
impl_repo!(
|
||||
@@ -1715,6 +1783,107 @@ mod tests {
|
||||
assert_eq!(hits[0].id, "k1");
|
||||
}
|
||||
|
||||
// ---------- 知识路标读侧(list_waymark,devflow.md 投影通道) ----------
|
||||
|
||||
/// 入选门槛:published 且(verified 或 复用达标);candidate/archived/复用不足均淘汰
|
||||
#[tokio::test]
|
||||
async fn waymark_qualifies_verified_or_reused_published_only() {
|
||||
let repo = setup_repo().await;
|
||||
// verified=true 复用 0 → 入选(验证豁免复用门槛)
|
||||
let mut v = krec("v", "已验证条目", "...", "lesson", "published", None, 0);
|
||||
v.verified = true;
|
||||
repo.insert(v).await.unwrap();
|
||||
// 未验证复用 3 → 入选(压线达标)
|
||||
repo.insert(krec("r3", "复用达标条目", "...", "lesson", "published", None, 3))
|
||||
.await
|
||||
.unwrap();
|
||||
// 未验证复用 2 → 淘汰
|
||||
repo.insert(krec("r2", "复用不足条目", "...", "lesson", "published", None, 2))
|
||||
.await
|
||||
.unwrap();
|
||||
// 复用再高,状态非 published(candidate)→ 淘汰
|
||||
repo.insert(krec("cand", "候选未发布", "...", "lesson", "candidate", None, 99))
|
||||
.await
|
||||
.unwrap();
|
||||
// archived → 淘汰
|
||||
repo.insert(krec("arch", "已归档", "...", "lesson", "archived", None, 99))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let hits = repo.list_waymark(None, 20, 10).await.unwrap();
|
||||
let ids: Vec<_> = hits.iter().map(|h| h.id.as_str()).collect();
|
||||
assert_eq!(ids.len(), 2, "只应入选 已验证 + 复用达标 两条");
|
||||
assert!(ids.contains(&"v") && ids.contains(&"r3"));
|
||||
}
|
||||
|
||||
/// 两档序:项目档在前(复用降序,verified 豁免复用但仍按复用参与排序),
|
||||
/// 跨项目补充档在后(复用降序);异项目高复用压不过同项目低复用(档位优先于复用)
|
||||
#[tokio::test]
|
||||
async fn waymark_project_tier_first_global_supplement_after() {
|
||||
let repo = setup_repo().await;
|
||||
repo.insert(krec("p9", "同项目复用9", "...", "lesson", "published", None, 9))
|
||||
.await
|
||||
.unwrap();
|
||||
repo.insert(krec("p5", "同项目复用5", "...", "lesson", "published", None, 5))
|
||||
.await
|
||||
.unwrap();
|
||||
// 同项目 verified 但零复用:入选项目档,排序落在复用条目之后
|
||||
let mut pv = krec("pv", "同项目已验证零复用", "...", "lesson", "published", None, 0);
|
||||
pv.verified = true;
|
||||
repo.insert(pv).await.unwrap();
|
||||
// 异项目高复用 → 补充档
|
||||
let mut f = krec("f99", "异项目复用99", "...", "lesson", "published", None, 99);
|
||||
f.source_project = Some("proj-2".to_string());
|
||||
repo.insert(f).await.unwrap();
|
||||
// 无归属全局条目 → 补充档
|
||||
let mut g = krec("g10", "全局复用10", "...", "lesson", "published", None, 10);
|
||||
g.source_project = None;
|
||||
repo.insert(g).await.unwrap();
|
||||
|
||||
let hits = repo.list_waymark(Some("proj-1"), 20, 10).await.unwrap();
|
||||
let ids: Vec<_> = hits.iter().map(|h| h.id.as_str()).collect();
|
||||
assert_eq!(
|
||||
ids,
|
||||
vec!["p9", "p5", "pv", "f99", "g10"],
|
||||
"项目档在前(复用降序),补充档在后(复用降序:异项目99 > 全局10)"
|
||||
);
|
||||
// 两档 WHERE 互斥的去重属性:任一条目恰好出现一次
|
||||
assert_eq!(ids.iter().filter(|i| **i == "p9").count(), 1, "同项目条目不得因两档重复出现");
|
||||
}
|
||||
|
||||
/// 各档上限独立生效:项目档取复用最高的 project_limit 条,补充档另计 global_limit 条
|
||||
/// (条目复用全部压线达标,确保裁剪只来自 LIMIT 而非入选门槛)
|
||||
#[tokio::test]
|
||||
async fn waymark_limits_cap_tiers_independently() {
|
||||
let repo = setup_repo().await;
|
||||
for reuse in 3..=5 {
|
||||
repo.insert(krec(&format!("p{reuse}"), "同项目", "...", "lesson", "published", None, reuse))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
for reuse in 3..=5 {
|
||||
let mut g = krec(&format!("g{reuse}"), "全局", "...", "lesson", "published", None, reuse);
|
||||
g.source_project = None;
|
||||
repo.insert(g).await.unwrap();
|
||||
}
|
||||
|
||||
let hits = repo.list_waymark(Some("proj-1"), 2, 2).await.unwrap();
|
||||
let ids: Vec<_> = hits.iter().map(|h| h.id.as_str()).collect();
|
||||
assert_eq!(
|
||||
ids,
|
||||
vec!["p5", "p4", "g5", "g4"],
|
||||
"项目档取复用最高的 2 条,补充档独立取全局最高的 2 条"
|
||||
);
|
||||
}
|
||||
|
||||
/// 空库 → 空结果(路标子块整体缺席的前提,投影侧据此零追加)
|
||||
#[tokio::test]
|
||||
async fn waymark_empty_db_returns_empty() {
|
||||
let repo = setup_repo().await;
|
||||
assert!(repo.list_waymark(Some("proj-1"), 20, 10).await.unwrap().is_empty());
|
||||
assert!(repo.list_waymark(None, 20, 10).await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
// ---------- 复用即信任(auto_publish_if_reused) ----------
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user