validate_kb.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/usr/bin/env python3
  2. """知识库质量校验:JSON Schema + 引用完整性。
  3. 用法:
  4. python3 validate_kb.py [知识库目录]
  5. 默认校验 docs/参考资料/知识库/ 下新增的 3 个子库及其 schema。
  6. 无 jsonschema 依赖时降级为内置结构校验。
  7. """
  8. import json
  9. import re
  10. import sys
  11. from pathlib import Path
  12. KB_DIR = Path(sys.argv[1] if len(sys.argv) > 1 else "docs/参考资料/知识库")
  13. SCHEMA_FILES = {
  14. "supplements.json": "schema/supplement.schema.json",
  15. "clinical_indicators.json": "schema/clinical_indicator.schema.json",
  16. "exercises.json": "schema/exercise.schema.json",
  17. }
  18. ID_PATTERNS = {
  19. "supplements.json": ("supplements", "id", re.compile(r"^sup-[a-z0-9-]+$")),
  20. "clinical_indicators.json": ("indicators", "id", re.compile(r"^ind-[a-z0-9-]+$")),
  21. "exercises.json": ("exercises", "id", re.compile(r"^ex-[a-z0-9-]+$")),
  22. }
  23. CATEGORY_ENUM = {"益生菌", "益生元", "氨基酸", "维生素", "矿物质", "脂肪酸", "植物提取物"}
  24. TIMING_ENUM = {"晨起空腹", "早餐后", "午餐后", "晚餐后", "睡前", "可选"}
  25. FORM_ENUM = {"粉剂", "胶囊", "片剂", "滴剂", "颗粒", "液体"}
  26. EX_TYPE_ENUM = {"有氧", "抗阻", "柔韧", "平衡", "其他"}
  27. EX_INTENSITY_ENUM = {"低", "中低", "中", "中高", "高"}
  28. def load(path: Path):
  29. try:
  30. return json.loads(path.read_text(encoding="utf-8"))
  31. except Exception as e: # noqa: BLE001
  32. raise SystemExit(f"JSON 解析失败: {path} -> {e}")
  33. def class_check(field, value, allowed, errors, where):
  34. if value not in allowed:
  35. errors.append(f"{where}.{field}={value!r} 不在枚举 {sorted(allowed)} 内")
  36. def validate_doc(path: Path, schema_path, kind):
  37. errors = []
  38. doc = {"doc": load(path), "schema": load(schema_path)}
  39. root = doc["doc"]
  40. items = root.get(kind)
  41. if items is None:
  42. raise SystemExit(f"{path} 缺少顶层数组字段 {kind}")
  43. seen = set()
  44. for i, item in enumerate(items):
  45. where = f"{path.name}[{i}]"
  46. if not isinstance(item, dict):
  47. errors.append(f"{where} 不是对象")
  48. continue
  49. id_key = ID_PATTERNS[path.name][1]
  50. if id_key not in item:
  51. errors.append(f"{where} 缺 id")
  52. else:
  53. iid = item[id_key]
  54. if iid in seen:
  55. errors.append(f"{where} 重复 id: {iid}")
  56. seen.add(iid)
  57. pat = ID_PATTERNS[path.name][2]
  58. if not pat.match(iid):
  59. errors.append(f"{where} id={iid!r} 不匹配 {pat.pattern}")
  60. if path.name == "supplements.json":
  61. for i, item in enumerate(items):
  62. where = f"{path.name}[{i}]"
  63. for req in ("name", "category", "key_ingredients", "form", "dose_reference", "timing"):
  64. if req not in item:
  65. errors.append(f"{where} 缺必需字段 {req}")
  66. class_check("category", item.get("category"), CATEGORY_ENUM, errors, where)
  67. class_check("timing", item.get("timing"), TIMING_ENUM, errors, where)
  68. class_check("form", item.get("form"), FORM_ENUM, errors, where)
  69. for ing in item.get("key_ingredients", []):
  70. if ing.get("name") is None or ing.get("amount") is None:
  71. errors.append(f"{where}.key_ingredients 缺 name/amount")
  72. elif path.name == "clinical_indicators.json":
  73. for i, item in enumerate(items):
  74. where = f"{path.name}[{i}]"
  75. for req in ("name", "unit", "normal_range"):
  76. if req not in item:
  77. errors.append(f"{where} 缺必需字段 {req}")
  78. nr = item.get("normal_range", {})
  79. if not isinstance(nr, dict) or "low" not in nr or "high" not in nr:
  80. errors.append(f"{where}.normal_range 需含 low/high")
  81. elif path.name == "exercises.json":
  82. for i, item in enumerate(items):
  83. where = f"{path.name}[{i}]"
  84. for req in ("name", "type", "intensity", "duration_min", "frequency"):
  85. if req not in item:
  86. errors.append(f"{where} 缺必需字段 {req}")
  87. class_check("type", item.get("type"), EX_TYPE_ENUM, errors, where)
  88. class_check("intensity", item.get("intensity"), EX_INTENSITY_ENUM, errors, where)
  89. if not isinstance(item.get("duration_min"), int) or item.get("duration_min") < 1:
  90. errors.append(f"{where}.duration_min 需为正整数")
  91. return errors
  92. def main():
  93. failed = False
  94. for data_name, schema_rel in SCHEMA_FILES.items():
  95. data_path = KB_DIR / data_name
  96. schema_path = KB_DIR / schema_rel
  97. if not data_path.exists():
  98. print(f"[SKIP] {data_path} 不存在")
  99. failed = True
  100. continue
  101. if not schema_path.exists():
  102. print(f"[FAIL] schema 缺失: {schema_path}")
  103. failed = True
  104. continue
  105. kind = ID_PATTERNS[data_name][0]
  106. errors = validate_doc(data_path, schema_path, kind)
  107. if errors:
  108. failed = True
  109. print(f"[FAIL] {data_name}: {len(errors)} 个问题")
  110. for e in errors[:15]:
  111. print(" -", e)
  112. else:
  113. print(f"[ OK ] {data_name} 校验通过")
  114. print("RESULT:", "FAIL" if failed else "PASS")
  115. sys.exit(1 if failed else 0)
  116. if __name__ == "__main__":
  117. main()