Skip to content

Commit 7d06509

Browse files
committed
feat(knowledge): 体系层数据模型与纯函数
1 parent 6ac2aac commit 7d06509

18 files changed

Lines changed: 2451 additions & 5 deletions
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! 知识审计数据层(v0.13.1 REQ-202;db_* 拆分模式同款)。
2+
//!
3+
//! @ai-context: knowledge_audits 表读写。v0.13.1 仅留表(审计界面/结算属 v0.13.4),
4+
//! 本层已备好"记录 + 最近审计时刻"读取,供 audit_due 探测用
5+
//! (M2 `audit_due_for_system` 前置读)。
6+
//! @ai-context: 时间戳一律 unix_seconds()(秒);latest_audit_at_ms 特供审计信号
7+
//! 纯函数(毫秒口径),故换算返回。
8+
//! @ai-context: 锁访问统一走 Db::with_conn(中毒锁恢复而非 panic)。
9+
//!
10+
//! @ai-context: M1 数据层 API 供 M2 command 层接入用(机制先行);接入后移除本 allow。
11+
#![allow(dead_code)]
12+
13+
use rusqlite::params;
14+
15+
use crate::db::{unix_seconds, Db};
16+
use crate::error::Result;
17+
use crate::types::KnowledgeAudit;
18+
19+
/// knowledge_audits 表统一查询列(列顺序与 row_to_audit 严格对应)。
20+
const AUDIT_COLUMNS: &str = "id, system_id, items_json, stats_json, created_at";
21+
22+
impl Db {
23+
/// 记录一次审计(v0.13.4 起 items/stats 有真实载荷;v0.13.1 先落空档)。
24+
pub fn add_knowledge_audit(
25+
&self,
26+
system_id: i64,
27+
items_json: &str,
28+
stats_json: &str,
29+
) -> Result<KnowledgeAudit> {
30+
let now = unix_seconds();
31+
self.with_conn(|conn| {
32+
conn.execute(
33+
"INSERT INTO knowledge_audits (system_id, items_json, stats_json, created_at)
34+
VALUES (?1, ?2, ?3, ?4)",
35+
params![system_id, items_json, stats_json, now],
36+
)?;
37+
let id = conn.last_insert_rowid();
38+
Ok(KnowledgeAudit {
39+
id,
40+
system_id,
41+
items_json: items_json.to_string(),
42+
stats_json: stats_json.to_string(),
43+
created_at: now,
44+
})
45+
})
46+
}
47+
48+
/// 列出体系审计记录(新→旧)。
49+
pub fn list_knowledge_audits(&self, system_id: i64) -> Result<Vec<KnowledgeAudit>> {
50+
self.with_conn(|conn| {
51+
let mut stmt = conn.prepare(&format!(
52+
"SELECT {} FROM knowledge_audits WHERE system_id = ?1 ORDER BY created_at DESC, id DESC",
53+
AUDIT_COLUMNS
54+
))?;
55+
let rows = stmt.query_map(params![system_id], row_to_audit)?;
56+
rows.collect::<rusqlite::Result<Vec<_>>>().map_err(Into::into)
57+
})
58+
}
59+
60+
/// 最近审计时刻(毫秒;None=从未审计——审计信号 last_audit_at_ms 用)。
61+
///
62+
/// @ai-context: DB 以秒存储(unix_seconds 惯例),审计信号纯函数 audit_due
63+
/// 约定毫秒口径(spec §三 AuditSignal)——此处换算为毫秒返回,
64+
/// M2 探测直接喂入 audit_due 无需再转。
65+
pub fn latest_audit_at_ms(&self, system_id: i64) -> Result<Option<i64>> {
66+
self.with_conn(|conn| {
67+
let mut stmt = conn.prepare(
68+
"SELECT created_at FROM knowledge_audits WHERE system_id = ?1
69+
ORDER BY created_at DESC, id DESC LIMIT 1",
70+
)?;
71+
let mut rows = stmt.query_map(params![system_id], |r| r.get::<_, i64>(0))?;
72+
match rows.next() {
73+
Some(Ok(secs)) => Ok(Some(secs * 1000)),
74+
Some(Err(e)) => Err(e.into()),
75+
None => Ok(None),
76+
}
77+
})
78+
}
79+
}
80+
81+
/// 把 rusqlite 行映射为 KnowledgeAudit。
82+
fn row_to_audit(row: &rusqlite::Row<'_>) -> rusqlite::Result<KnowledgeAudit> {
83+
Ok(KnowledgeAudit {
84+
id: row.get(0)?,
85+
system_id: row.get(1)?,
86+
items_json: row.get(2)?,
87+
stats_json: row.get(3)?,
88+
created_at: row.get(4)?,
89+
})
90+
}
91+
92+
/// 单测独立文件(保持本文件 ≤300 行,AGENTS.md §3)。
93+
#[cfg(test)]
94+
#[path = "db_knowledge_audits_tests.rs"]
95+
mod tests;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//! db_knowledge_audits 单测(内存库;AAA 模式)。
2+
3+
use crate::db::Db;
4+
use crate::types::NewKnowledgeSystem;
5+
6+
/// 内存库。
7+
fn mem_db() -> Db {
8+
Db::open(":memory:").expect("内存库打开")
9+
}
10+
11+
/// 建一个体系并返回。
12+
fn host_system(db: &Db) -> i64 {
13+
db.create_knowledge_system(&NewKnowledgeSystem {
14+
name: "体系".to_string(),
15+
kind: "domain".to_string(),
16+
parent_system_id: None,
17+
core_question: None,
18+
})
19+
.expect("create system")
20+
.id
21+
}
22+
23+
#[test]
24+
fn add_and_list_roundtrip() {
25+
// Arrange
26+
let db = mem_db();
27+
let sid = host_system(&db);
28+
// Act
29+
let a = db
30+
.add_knowledge_audit(sid, r#"["n1","n2"]"#, r#"{"done":2}"#)
31+
.expect("add first");
32+
let b = db
33+
.add_knowledge_audit(sid, r#"["n3"]"#, r#"{}"#)
34+
.expect("add second");
35+
let audits = db.list_knowledge_audits(sid).expect("list");
36+
// Assert:新→旧排序(b 在前),字段往返
37+
assert_eq!(audits.len(), 2);
38+
assert_eq!(audits[0].id, b.id);
39+
assert_eq!(audits[1].id, a.id);
40+
assert_eq!(audits[1].items_json, r#"["n1","n2"]"#);
41+
}
42+
43+
#[test]
44+
fn latest_audit_none_when_never_audited() {
45+
// Arrange:无审计记录
46+
let db = mem_db();
47+
let sid = host_system(&db);
48+
// Act
49+
let latest = db.latest_audit_at_ms(sid).expect("latest");
50+
// Assert:从未审计 → None
51+
assert_eq!(latest, None);
52+
}
53+
54+
#[test]
55+
fn latest_audit_returns_most_recent_in_ms() {
56+
// Arrange:两次审计(时间递增由真实时钟保证)
57+
let db = mem_db();
58+
let sid = host_system(&db);
59+
db.add_knowledge_audit(sid, "[]", "{}").expect("a1");
60+
// Act:取最近
61+
let latest = db.latest_audit_at_ms(sid).expect("latest").expect("exists");
62+
// Assert:返回毫秒(秒*1000——审计信号口径);>0 且为 1000 的倍数
63+
assert!(latest > 0);
64+
assert_eq!(latest % 1000, 0);
65+
}
66+
67+
#[test]
68+
fn audits_scoped_to_system() {
69+
// Arrange:两体系各有审计
70+
let db = mem_db();
71+
let sid1 = host_system(&db);
72+
let sid2 = host_system(&db);
73+
db.add_knowledge_audit(sid1, "[]", "{}").expect("s1");
74+
db.add_knowledge_audit(sid2, "[]", "{}").expect("s2");
75+
// Act
76+
let in_s1 = db.list_knowledge_audits(sid1).expect("list s1");
77+
// Assert:按体系隔离
78+
assert_eq!(in_s1.len(), 1);
79+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//! 知识概念数据层(v0.13.1 REQ-202;db_* 拆分模式同款)。
2+
//!
3+
//! @ai-context: knowledge_concepts 表 CRUD + 按名查找。概念是全库唯一身份的
4+
//! 知识单元(三问本质/边界/联系),name UNIQUE(spec §二 不可变约束)
5+
//! 是交叉点判定的前提——重复名冲突由 rusqlite 约束错误上抛(不过滤)。
6+
//! @ai-context: 锁访问统一走 Db::with_conn(中毒锁恢复而非 panic)。
7+
//!
8+
//! @ai-context: M1 数据层 API 供 M2 command 层接入用(机制先行);接入后移除本 allow。
9+
#![allow(dead_code)]
10+
11+
use rusqlite::params;
12+
13+
use crate::db::{unix_seconds, Db};
14+
use crate::error::Result;
15+
use crate::types::{KnowledgeConcept, NewKnowledgeConcept};
16+
17+
/// knowledge_concepts 表统一查询列(列顺序与 row_to_concept 严格对应)。
18+
const CONCEPT_COLUMNS: &str =
19+
"id, system_id, name, essence, boundary, relation, status, last_applied_at, created_at, updated_at";
20+
21+
impl Db {
22+
/// 新建概念,返回完整记录。
23+
///
24+
/// @ai-context: name 全局 UNIQUE——同名概念重复创建时 rusqlite 约束错误自然上抛
25+
/// (command 层预查给友好错误;名称归一化在 command 层执行后落库)。
26+
pub fn add_knowledge_concept(&self, new: &NewKnowledgeConcept) -> Result<KnowledgeConcept> {
27+
let now = unix_seconds();
28+
self.with_conn(|conn| {
29+
conn.execute(
30+
"INSERT INTO knowledge_concepts (system_id, name, essence, boundary, relation, status, last_applied_at, created_at, updated_at)
31+
VALUES (?1, ?2, ?3, ?4, ?5, 'core', NULL, ?6, ?6)",
32+
params![new.system_id, new.name, new.essence, new.boundary, new.relation, now],
33+
)?;
34+
let id = conn.last_insert_rowid();
35+
Ok(KnowledgeConcept {
36+
id,
37+
system_id: new.system_id,
38+
name: new.name.clone(),
39+
essence: new.essence.clone(),
40+
boundary: new.boundary.clone(),
41+
relation: new.relation.clone(),
42+
status: "core".to_string(),
43+
last_applied_at: None,
44+
created_at: now,
45+
updated_at: now,
46+
})
47+
})
48+
}
49+
50+
/// 按 id 读取概念;不存在返回 None。
51+
pub fn get_knowledge_concept(&self, id: i64) -> Result<Option<KnowledgeConcept>> {
52+
self.with_conn(|conn| {
53+
let mut stmt = conn.prepare(&format!(
54+
"SELECT {} FROM knowledge_concepts WHERE id = ?1",
55+
CONCEPT_COLUMNS
56+
))?;
57+
let mut rows = stmt.query_map(params![id], row_to_concept)?;
58+
match rows.next() {
59+
Some(Ok(c)) => Ok(Some(c)),
60+
Some(Err(e)) => Err(e.into()),
61+
None => Ok(None),
62+
}
63+
})
64+
}
65+
66+
/// 按名称查找概念(全局唯一——命中至多一条;名称已归一化由调用方保证)。
67+
pub fn find_concept_by_name(&self, name: &str) -> Result<Option<KnowledgeConcept>> {
68+
self.with_conn(|conn| {
69+
let mut stmt = conn.prepare(&format!(
70+
"SELECT {} FROM knowledge_concepts WHERE name = ?1 LIMIT 1",
71+
CONCEPT_COLUMNS
72+
))?;
73+
let mut rows = stmt.query_map(params![name], row_to_concept)?;
74+
match rows.next() {
75+
Some(Ok(c)) => Ok(Some(c)),
76+
Some(Err(e)) => Err(e.into()),
77+
None => Ok(None),
78+
}
79+
})
80+
}
81+
82+
/// 更新概念可选字段。
83+
///
84+
/// @ai-context: 三问 essence/boundary/relation 为可空列(两层 Option:外层 None=不改,
85+
/// 内层 None=清空为 NULL);name 为 NOT NULL(单层 Option:None=不改)。
86+
pub fn update_knowledge_concept(
87+
&self,
88+
id: i64,
89+
name: Option<&str>,
90+
essence: Option<Option<&str>>,
91+
boundary: Option<Option<&str>>,
92+
relation: Option<Option<&str>>,
93+
status: Option<&str>,
94+
) -> Result<bool> {
95+
self.with_conn(|conn| {
96+
let mut sets: Vec<String> = Vec::new();
97+
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
98+
if let Some(v) = name {
99+
sets.push(format!("name = ?{}", params.len() + 1));
100+
params.push(Box::new(v.to_string()));
101+
}
102+
if let Some(v) = essence {
103+
sets.push(format!("essence = ?{}", params.len() + 1));
104+
params.push(Box::new(v.map(str::to_string)));
105+
}
106+
if let Some(v) = boundary {
107+
sets.push(format!("boundary = ?{}", params.len() + 1));
108+
params.push(Box::new(v.map(str::to_string)));
109+
}
110+
if let Some(v) = relation {
111+
sets.push(format!("relation = ?{}", params.len() + 1));
112+
params.push(Box::new(v.map(str::to_string)));
113+
}
114+
if let Some(v) = status {
115+
sets.push(format!("status = ?{}", params.len() + 1));
116+
params.push(Box::new(v.to_string()));
117+
}
118+
if sets.is_empty() {
119+
return Ok(false);
120+
}
121+
sets.push(format!("updated_at = ?{}", params.len() + 1));
122+
params.push(Box::new(unix_seconds()));
123+
let sql = format!(
124+
"UPDATE knowledge_concepts SET {} WHERE id = ?{}",
125+
sets.join(", "),
126+
params.len() + 1
127+
);
128+
params.push(Box::new(id));
129+
let affected = conn.execute(&sql, rusqlite::params_from_iter(params.iter().map(|b| b.as_ref())))?;
130+
Ok(affected > 0)
131+
})
132+
}
133+
134+
/// 列出概念(体系过滤可选、状态过滤可选;动态 WHERE)。
135+
pub fn list_knowledge_concepts(
136+
&self,
137+
system_id: Option<i64>,
138+
status: Option<&str>,
139+
) -> Result<Vec<KnowledgeConcept>> {
140+
self.with_conn(|conn| {
141+
let mut clauses: Vec<String> = Vec::new();
142+
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
143+
if let Some(sid) = system_id {
144+
clauses.push(format!("system_id = ?{}", params.len() + 1));
145+
params.push(Box::new(sid));
146+
}
147+
if let Some(st) = status {
148+
clauses.push(format!("status = ?{}", params.len() + 1));
149+
params.push(Box::new(st.to_string()));
150+
}
151+
let where_sql = if clauses.is_empty() {
152+
String::new()
153+
} else {
154+
format!("WHERE {}", clauses.join(" AND "))
155+
};
156+
let sql = format!(
157+
"SELECT {} FROM knowledge_concepts {} ORDER BY id ASC",
158+
CONCEPT_COLUMNS, where_sql
159+
);
160+
let mut stmt = conn.prepare(&sql)?;
161+
let rows = stmt.query_map(rusqlite::params_from_iter(params.iter().map(|b| b.as_ref())), row_to_concept)?;
162+
rows.collect::<rusqlite::Result<Vec<_>>>().map_err(Into::into)
163+
})
164+
}
165+
}
166+
167+
/// 把 rusqlite 行映射为 KnowledgeConcept。
168+
fn row_to_concept(row: &rusqlite::Row<'_>) -> rusqlite::Result<KnowledgeConcept> {
169+
Ok(KnowledgeConcept {
170+
id: row.get(0)?,
171+
system_id: row.get(1)?,
172+
name: row.get(2)?,
173+
essence: row.get(3)?,
174+
boundary: row.get(4)?,
175+
relation: row.get(5)?,
176+
status: row.get(6)?,
177+
last_applied_at: row.get(7)?,
178+
created_at: row.get(8)?,
179+
updated_at: row.get(9)?,
180+
})
181+
}
182+
183+
/// 单测独立文件(保持本文件 ≤300 行,AGENTS.md §3)。
184+
#[cfg(test)]
185+
#[path = "db_knowledge_concepts_tests.rs"]
186+
mod tests;

0 commit comments

Comments
 (0)