Initial commit: company-kb framework

- 完整的知识库框架(P0-P5 路线图)
- 12篇治理文档
- 工具脚本(kb-init.sh, kb-lint-fm.py, feishu-sync.py, kb-bot)
- 7个 Claude 命令
- 示例项目和测试项目(wanniu-l1)
- 契约文件(meta/kb-contract.yaml)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-07-16 18:24:39 +08:00
commit 84f7950589
60 changed files with 4410 additions and 0 deletions

178
tools/kb-lint-fm.py Executable file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""kb-lint-fm.py —— frontmatter 校验器
单一真相源:读 meta/kb-contract.yaml 判定 type 白名单 / 必填矩阵 / enum
绝不硬编码 type 或字段清单。
用法:
python3 tools/kb-lint-fm.py [路径...] # 默认扫全库
python3 tools/kb-lint-fm.py projects/foo # 只扫某目录/文件
退出码:
0 = 全部通过
1 = 有错误(缺硬必填 / 未激活 type / enum 非法 / 派生必填缺失)
2 = 运行环境问题(缺 PyYAML / 缺契约文件)
供 CC 判定:非 0 即需处理。
"""
import sys
import os
import re
import fnmatch
try:
import yaml
except ImportError:
print("ERROR: 需要 PyYAML。请运行: pip install pyyaml", file=sys.stderr)
sys.exit(2)
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
CONTRACT_PATH = os.path.join(ROOT, "meta", "kb-contract.yaml")
FM_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
def load_contract():
if not os.path.isfile(CONTRACT_PATH):
print(f"ERROR: 缺契约文件 {CONTRACT_PATH}", file=sys.stderr)
sys.exit(2)
with open(CONTRACT_PATH, encoding="utf-8") as f:
return yaml.safe_load(f)
def is_exempt(rel, patterns):
for pat in patterns:
if fnmatch.fnmatch(rel, pat) or fnmatch.fnmatch(rel, pat.rstrip("/") + "/*"):
return True
# 目录型 globdocs/**)也匹配 docs/ 下任意层
for pat in patterns:
if pat.endswith("/**") and rel.startswith(pat[:-2]):
return True
return False
def parse_frontmatter(text):
m = FM_RE.match(text)
if not m:
return None, "缺 frontmatter 块(--- 开头)"
try:
data = yaml.safe_load(m.group(1))
except yaml.YAMLError as e:
return None, f"frontmatter YAML 解析失败: {e}"
if not isinstance(data, dict):
return None, "frontmatter 不是键值映射"
return data, None
def collect_md(paths):
files = []
for p in paths:
ap = os.path.abspath(p)
if os.path.isfile(ap) and ap.endswith(".md"):
files.append(ap)
elif os.path.isdir(ap):
for dp, _, fns in os.walk(ap):
for fn in fns:
if fn.endswith(".md"):
files.append(os.path.join(dp, fn))
return sorted(set(files))
def check_derived(data, rules):
"""返回派生必填字段名列表"""
req = []
t = data.get("type")
src = data.get("source")
for rule in rules or []:
w = rule.get("when", {})
ok = True
if "type_in" in w and t not in w["type_in"]:
ok = False
if "source_in" in w and src not in w["source_in"]:
ok = False
if ok:
req.extend(rule.get("require", []))
return req
def main():
contract = load_contract()
types = {t["name"]: t for t in contract.get("types", [])}
active = {n for n, t in types.items() if t.get("active")}
fields = contract.get("fields", {})
enums = contract.get("enums", {})
exempt = contract.get("frontmatter_exempt", [])
source_exempt_types = set(contract.get("source_exempt_types", []))
derived = contract.get("derived_rules", [])
always_required = [f for f, spec in fields.items()
if spec.get("required") == "always"]
targets = sys.argv[1:] or [ROOT]
files = collect_md(targets)
errors = 0
warnings = 0
checked = 0
for fp in files:
rel = os.path.relpath(fp, ROOT)
if is_exempt(rel, exempt):
continue
checked += 1
with open(fp, encoding="utf-8") as f:
text = f.read()
data, err = parse_frontmatter(text)
if err:
print(f"[ERROR] {rel}: {err}")
errors += 1
continue
# type 硬必填 + 白名单
t = data.get("type")
if not t:
print(f"[ERROR] {rel}: 缺硬必填字段 type")
errors += 1
continue
if t not in types:
print(f"[ERROR] {rel}: 未知 type '{t}'(不在契约中,扩展需改 kb-contract.yaml")
errors += 1
elif t not in active:
print(f"[ERROR] {rel}: type '{t}' 已登记但未激活(改契约 active: true 再用)")
errors += 1
# 其余硬必填
for f_ in always_required:
if f_ == "type":
continue
if data.get(f_) in (None, ""):
print(f"[ERROR] {rel}: 缺硬必填字段 {f_}")
errors += 1
# 派生必填project 型豁免溯源)
req = check_derived(data, derived)
if t in source_exempt_types:
req = [r for r in req if r not in ("source", "source_link")]
for f_ in req:
if data.get(f_) in (None, ""):
print(f"[ERROR] {rel}: 派生必填缺失 {f_}type={t} + source 条件触发)")
errors += 1
# enum 校验
for f_, allowed in enums.items():
v = data.get(f_)
if v is not None and v not in allowed:
print(f"[ERROR] {rel}: 字段 {f_}='{v}' 不在 enum {allowed}")
errors += 1
# 软提示:建议但非必填的 title
if not data.get("title"):
print(f"[WARN] {rel}: 建议补 titleAI 可自动=H1 或文件名)")
warnings += 1
print(f"\n检查 {checked} 页 | 错误 {errors} | 警告 {warnings}")
sys.exit(1 if errors else 0)
if __name__ == "__main__":
main()