Initial commit

This commit is contained in:
2026-07-20 15:26:30 +08:00
commit 5629342035
11 changed files with 1316 additions and 0 deletions

174
scripts/validate.py Executable file
View File

@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Validate the structure and evidence links of the AI distillate."""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
LEARNINGS = ROOT / "learnings"
REQUIRED_FILES = (
ROOT / "README.md",
ROOT / "AGENTS.md",
ROOT / "TASTE.md",
LEARNINGS / "README.md",
)
RECORD_NAME = re.compile(
r"^(?P<date>\d{4}-\d{2}-\d{2})-(?P<number>\d{3})-"
r"(?P<slug>[a-z0-9][a-z0-9-]*)\.md$"
)
RECORD_HEADINGS = (
"# ",
"## 原始输入",
"## 内容蒸馏",
"## Taste 信号",
"### 明确偏好",
"### 推断信号",
"### 领域知识",
"## 边界与反例",
"## 可执行影响",
"## 画像更新",
)
PROFILE_LABELS = ("状态", "适用范围", "规则", "证据", "边界")
PROFILE_STATUSES = {"确认", "暂定", "有条件", "已取代"}
EVIDENCE_LINK = re.compile(r"\]\((learnings/[^)#]+\.md)(?:#[^)]+)?\)")
def parse_front_matter(text: str) -> dict[str, str] | None:
if not text.startswith("---\n"):
return None
closing = text.find("\n---\n", 4)
if closing == -1:
return None
fields: dict[str, str] = {}
for line in text[4:closing].splitlines():
if ":" in line and not line.startswith((" ", "\t")):
key, value = line.split(":", 1)
fields[key.strip()] = value.strip()
return fields
def validate_record(record: Path) -> list[str]:
errors: list[str] = []
match = RECORD_NAME.fullmatch(record.name)
if match is None:
return [f"{record.relative_to(ROOT)}: 文件名不符合约定"]
text = record.read_text(encoding="utf-8")
fields = parse_front_matter(text)
if fields is None:
return [f"{record.relative_to(ROOT)}: 缺少完整的 YAML front matter"]
expected_id = record.name[:14]
required_fields = {"id", "date", "status", "tags", "sources", "references"}
missing_fields = sorted(required_fields - fields.keys())
if missing_fields:
errors.append(
f"{record.relative_to(ROOT)}: 缺少字段 {', '.join(missing_fields)}"
)
if fields.get("id") != expected_id:
errors.append(
f"{record.relative_to(ROOT)}: id 应为 {expected_id}"
)
if fields.get("date") != match.group("date"):
errors.append(
f"{record.relative_to(ROOT)}: date 应与文件名日期一致"
)
if fields.get("status") not in {"distilled", "needs-evidence"}:
errors.append(
f"{record.relative_to(ROOT)}: status 必须是 distilled 或 needs-evidence"
)
lines = text.splitlines()
for heading in RECORD_HEADINGS:
if heading == "# ":
if not any(line.startswith("# ") for line in lines):
errors.append(f"{record.relative_to(ROOT)}: 缺少一级标题")
elif heading not in lines:
errors.append(f"{record.relative_to(ROOT)}: 缺少标题 {heading}")
return errors
def validate_profile() -> list[str]:
profile_path = ROOT / "TASTE.md"
text = profile_path.read_text(encoding="utf-8")
marker = "## 当前画像"
if marker not in text:
return ["TASTE.md: 缺少‘当前画像’部分"]
current = text.split(marker, 1)[1].strip()
placeholder = "尚无通过 `学习xxx` 沉淀的条目。"
if current == placeholder:
return []
if placeholder in current:
return ["TASTE.md: 已有画像条目时应删除空画像占位文字"]
starts = list(re.finditer(r"(?m)^### (.+)$", current))
if not starts:
return ["TASTE.md: 当前画像必须使用三级标题组织条目"]
errors: list[str] = []
for index, start in enumerate(starts):
end = starts[index + 1].start() if index + 1 < len(starts) else len(current)
title = start.group(1)
entry = current[start.end():end]
values: dict[str, str] = {}
for label in PROFILE_LABELS:
match = re.search(rf"(?m)^- {re.escape(label)}(.+)$", entry)
if match is None:
errors.append(f"TASTE.md / {title}: 缺少‘{label}")
else:
values[label] = match.group(1).strip()
if "状态" in values and values["状态"] not in PROFILE_STATUSES:
errors.append(f"TASTE.md / {title}: 状态值无效")
if "证据" in values and EVIDENCE_LINK.search(values["证据"]) is None:
errors.append(f"TASTE.md / {title}: 证据必须链接到 learnings/ 记录")
return errors
def validate_evidence_links() -> list[str]:
text = (ROOT / "TASTE.md").read_text(encoding="utf-8")
marker = "## 当前画像"
if marker in text:
text = text.split(marker, 1)[1]
errors: list[str] = []
for relative_path in EVIDENCE_LINK.findall(text):
if not (ROOT / relative_path).is_file():
errors.append(f"TASTE.md: 证据链接不存在:{relative_path}")
return errors
def main() -> int:
errors = [
f"缺少必要文件:{path.relative_to(ROOT)}"
for path in REQUIRED_FILES
if not path.is_file()
]
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
records = sorted(
path for path in LEARNINGS.glob("*.md") if path.name != "README.md"
)
for record in records:
errors.extend(validate_record(record))
errors.extend(validate_profile())
errors.extend(validate_evidence_links())
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print(f"OK: {len(records)} learning record(s), profile structure is valid")
return 0
if __name__ == "__main__":
raise SystemExit(main())