vscode-templete/.claude/skills/auto-wiki-cn/references/store.py

308 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Wiki 结构化数据存储层。
每个 wiki 目录下维护一个 data.dbSQLite存储所有结构化数据。
Markdown 页面只负责叙事分析,不在 frontmatter 中存储 data/history。
用法:
from store import WikiStore
store = WikiStore(".wiki/my-research/")
store.upsert_data("alpha-corp", "管理规模", 1200, "亿元", "2025-12", "2026-04-policy-doc")
store.add_relation("alpha-corp", "受托人市场格局", "part_of")
# 查询
rows = store.query_data(page_slug="alpha-corp")
timeline = store.query_timeline(field="管理规模")
CLI:
python store.py init .wiki/my-research/
python store.py dump .wiki/my-research/
"""
from __future__ import annotations
import sqlite3
import sys
from datetime import date, datetime
from pathlib import Path
from typing import Any, Optional
SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS pages (
slug TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('source','entity','concept','analysis','mental-model')),
confidence TEXT NOT NULL DEFAULT 'medium' CHECK(confidence IN ('high','medium','low','contested')),
created TEXT NOT NULL,
updated TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS data_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
page_slug TEXT NOT NULL REFERENCES pages(slug),
field TEXT NOT NULL,
value REAL NOT NULL,
unit TEXT NOT NULL,
period TEXT NOT NULL,
source_slug TEXT NOT NULL,
scope TEXT,
verified INTEGER, -- NULL=unknown, 0=false, 1=true
confidence TEXT DEFAULT 'high' CHECK(confidence IN ('high','medium','low','contested')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(page_slug, field, period) -- 同一页面同一字段同一时段只留一条upsert 覆盖)
);
CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
page_slug TEXT NOT NULL REFERENCES pages(slug),
field TEXT NOT NULL,
old_value REAL NOT NULL,
old_unit TEXT NOT NULL,
old_source TEXT NOT NULL,
new_source TEXT,
reason TEXT NOT NULL,
date TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_slug TEXT NOT NULL,
to_slug TEXT NOT NULL,
type TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(from_slug, to_slug, type)
);
CREATE INDEX IF NOT EXISTS idx_dp_page ON data_points(page_slug);
CREATE INDEX IF NOT EXISTS idx_dp_field ON data_points(field);
CREATE INDEX IF NOT EXISTS idx_dp_period ON data_points(period);
CREATE INDEX IF NOT EXISTS idx_rel_from ON relations(from_slug);
CREATE INDEX IF NOT EXISTS idx_rel_to ON relations(to_slug);
CREATE INDEX IF NOT EXISTS idx_hist_page ON history(page_slug);
"""
class WikiStore:
"""单个 wiki 的 SQLite 存储接口。"""
def __init__(self, wiki_dir: str | Path):
self.wiki_dir = Path(wiki_dir)
self.db_path = self.wiki_dir / "data.db"
self._conn: Optional[sqlite3.Connection] = None
@property
def conn(self) -> sqlite3.Connection:
if self._conn is None:
self._conn = sqlite3.connect(str(self.db_path))
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA journal_mode=WAL")
self._conn.execute("PRAGMA foreign_keys=ON")
return self._conn
def init_db(self) -> None:
"""创建表结构(幂等)。"""
self.conn.executescript(SCHEMA_SQL)
self.conn.commit()
def close(self) -> None:
if self._conn:
self._conn.close()
self._conn = None
# ── Pages ──
def upsert_page(self, slug: str, title: str, page_type: str,
confidence: str = "medium",
created: str = "", updated: str = "") -> None:
today = date.today().isoformat()
self.conn.execute("""
INSERT INTO pages (slug, title, type, confidence, created, updated)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(slug) DO UPDATE SET
title=excluded.title, type=excluded.type,
confidence=excluded.confidence, updated=excluded.updated
""", (slug, title, page_type, confidence, created or today, updated or today))
self.conn.commit()
# ── Data Points ──
def upsert_data(self, page_slug: str, field: str, value: float,
unit: str, period: str, source_slug: str,
scope: str = None, verified: bool = None,
confidence: str = "high") -> Optional[dict]:
"""写入数据点。如果同字段同时段已有旧值,自动写入 history 并返回旧记录。"""
# 查旧值
old = self.conn.execute(
"SELECT value, unit, source_slug FROM data_points WHERE page_slug=? AND field=? AND period=?",
(page_slug, field, period)
).fetchone()
old_record = None
if old and old["value"] != value:
old_record = dict(old)
# 写 history
self.conn.execute("""
INSERT INTO history (page_slug, field, old_value, old_unit, old_source, new_source, reason, date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (page_slug, field, old["value"], old["unit"], old["source_slug"],
source_slug, f"{field}: {old['value']}{value}", date.today().isoformat()))
# upsert data point
v_int = None if verified is None else (1 if verified else 0)
self.conn.execute("""
INSERT INTO data_points (page_slug, field, value, unit, period, source_slug, scope, verified, confidence)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(page_slug, field, period) DO UPDATE SET
value=excluded.value, unit=excluded.unit,
source_slug=excluded.source_slug, scope=excluded.scope,
verified=excluded.verified, confidence=excluded.confidence
""", (page_slug, field, value, unit, period, source_slug, scope, v_int, confidence))
self.conn.commit()
return old_record
# ── Relations ──
def add_relation(self, from_slug: str, to_slug: str, rel_type: str) -> None:
self.conn.execute("""
INSERT OR IGNORE INTO relations (from_slug, to_slug, type)
VALUES (?, ?, ?)
""", (from_slug, to_slug, rel_type))
self.conn.commit()
# ── Queries ──
def query_data(self, page_slug: str = None, field: str = None) -> list[dict]:
"""查询数据点。可按页面或字段过滤。"""
sql = "SELECT * FROM data_points WHERE 1=1"
params: list = []
if page_slug:
sql += " AND page_slug=?"
params.append(page_slug)
if field:
sql += " AND field=?"
params.append(field)
sql += " ORDER BY period DESC"
return [dict(r) for r in self.conn.execute(sql, params).fetchall()]
def query_timeline(self, field: str, page_slug: str = None) -> list[dict]:
"""查询某字段的时间线(含历史值)。"""
# 当前值
sql = "SELECT page_slug, field, value, unit, period, source_slug, 'current' as status FROM data_points WHERE field=?"
params: list = [field]
if page_slug:
sql += " AND page_slug=?"
params.append(page_slug)
# 历史值
sql2 = "SELECT page_slug, field, old_value as value, old_unit as unit, date as period, old_source as source_slug, 'superseded' as status FROM history WHERE field=?"
params2: list = [field]
if page_slug:
sql2 += " AND page_slug=?"
params2.append(page_slug)
rows = [dict(r) for r in self.conn.execute(sql, params).fetchall()]
rows += [dict(r) for r in self.conn.execute(sql2, params2).fetchall()]
rows.sort(key=lambda r: r.get("period", ""), reverse=True)
return rows
def query_relations(self, slug: str = None, rel_type: str = None) -> list[dict]:
sql = "SELECT * FROM relations WHERE 1=1"
params: list = []
if slug:
sql += " AND (from_slug=? OR to_slug=?)"
params += [slug, slug]
if rel_type:
sql += " AND type=?"
params.append(rel_type)
return [dict(r) for r in self.conn.execute(sql, params).fetchall()]
def get_page(self, slug: str) -> Optional[dict]:
row = self.conn.execute("SELECT * FROM pages WHERE slug=?", (slug,)).fetchone()
return dict(row) if row else None
def list_pages(self, page_type: str = None) -> list[dict]:
sql = "SELECT * FROM pages"
params: list = []
if page_type:
sql += " WHERE type=?"
params.append(page_type)
sql += " ORDER BY updated DESC"
return [dict(r) for r in self.conn.execute(sql, params).fetchall()]
def stats(self) -> dict:
"""返回 wiki 数据库统计。"""
s: dict[str, Any] = {}
s["pages"] = self.conn.execute("SELECT COUNT(*) FROM pages").fetchone()[0]
s["data_points"] = self.conn.execute("SELECT COUNT(*) FROM data_points").fetchone()[0]
s["relations"] = self.conn.execute("SELECT COUNT(*) FROM relations").fetchone()[0]
s["contested"] = self.conn.execute("SELECT COUNT(*) FROM data_points WHERE confidence='contested'").fetchone()[0]
for row in self.conn.execute("SELECT type, COUNT(*) as cnt FROM pages GROUP BY type").fetchall():
s[f"pages_{row['type']}"] = row["cnt"]
return s
def dump(self) -> str:
"""输出人类可读的数据库摘要。"""
st = self.stats()
lines = [
f"Wiki Store: {self.wiki_dir.name}",
f"{'='*50}",
f"Pages: {st['pages']} | Data Points: {st['data_points']} | Relations: {st['relations']} | Contested: {st['contested']}",
"",
]
# pages by type
for pt in ["entity", "concept", "source", "analysis", "mental-model"]:
key = f"pages_{pt}"
if st.get(key):
lines.append(f" {pt}: {st[key]}")
# recent data points
recent = self.conn.execute(
"SELECT page_slug, field, value, unit, period FROM data_points ORDER BY created_at DESC LIMIT 10"
).fetchall()
if recent:
lines += ["", "Recent Data Points:"]
for r in recent:
lines.append(f" {r['page_slug']}.{r['field']} = {r['value']} {r['unit']} ({r['period']})")
# relations
rels = self.conn.execute("SELECT * FROM relations ORDER BY created_at DESC LIMIT 10").fetchall()
if rels:
lines += ["", "Recent Relations:"]
for r in rels:
lines.append(f" {r['from_slug']} --{r['type']}--> {r['to_slug']}")
return "\n".join(lines)
# ── CLI ──
def main():
if len(sys.argv) < 3:
print("Usage:")
print(" python store.py init <wiki_dir> — initialize data.db")
print(" python store.py dump <wiki_dir> — dump database summary")
sys.exit(1)
cmd, target = sys.argv[1], Path(sys.argv[2])
store = WikiStore(target)
if cmd == "init":
store.init_db()
print(f"Initialized: {store.db_path}")
elif cmd == "dump":
if not store.db_path.exists():
print(f"No data.db found in {target}")
sys.exit(1)
print(store.dump())
else:
print(f"Unknown command: {cmd}")
sys.exit(1)
store.close()
if __name__ == "__main__":
main()