petstore-docs/graph/validate_ontology.py

198 lines
6.5 KiB
Python
Executable File
Raw 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.

#!/usr/bin/env python3
"""
宠小它 知识本体校验脚本。
校验 docs/graph/ontology.jsonl 的:
1. JSONL 格式合法(每行一个 JSON 对象)
2. ID 唯一且符合命名规则entity:/action:/event:/rule:/rel:
3. 引用完整性:每个被引用的 IDsubject/object/inputs/outputs/appliesTo/appliesRules/emittedBy/appliesTo都存在
4. 证据强度分布统计
5. 类型必填字段检查Entity/Action/Event/Rule/Relation 各自的最小字段集)
用法:
python3 docs/graph/validate_ontology.py [docs_root]
默认 docs_root = docs相对当前工作目录
退出码0 = 全部通过1 = 有错误。
"""
import json
import os
import re
import sys
from collections import Counter
VALID_EVIDENCE = {"anchored", "documented", "implemented", "gap"}
VALID_TYPES = {"Entity", "Action", "Event", "Rule", "Relation"}
ID_PREFIX_BY_TYPE = {
"Entity": "entity:",
"Action": "action:",
"Event": "event:",
"Rule": "rule:",
"Relation": "rel:",
}
REQUIRED_FIELDS_BY_TYPE = {
"Entity": {"id", "type", "name", "fields", "evidence"},
"Action": {"id", "type", "name", "entrypoints", "evidence"},
"Event": {"id", "type", "name", "domain", "emittedBy", "evidence"},
"Rule": {"id", "type", "name", "appliesTo", "evidence"},
"Relation": {"id", "type", "subject", "predicate", "object", "evidence"},
}
# 引用字段:这些字段里看起来像本体 ID 的字符串(以 entity:/action:/event:/rule:/rel: 开头)必须存在
# 原始字段名(如 phone、code、token不视为引用跳过
REFERENCE_FIELDS = {
"inputs", "outputs", "appliesTo", "appliesRules", "emittedBy",
"subject", "object",
}
ID_PREFIXES = ("entity:", "action:", "event:", "rule:", "rel:")
def is_id_like(s):
return isinstance(s, str) and s.startswith(ID_PREFIXES)
def load_jsonl(path):
entries = []
errors = []
with open(path, "r", encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
obj["_lineno"] = lineno
entries.append(obj)
except json.JSONDecodeError as e:
errors.append(f"line {lineno}: invalid JSON - {e}")
return entries, errors
def validate_ids(entries):
errors = []
ids = set()
for e in entries:
eid = e.get("id")
if not eid:
errors.append(f"line {e['_lineno']}: missing id")
continue
if eid in ids:
errors.append(f"line {e['_lineno']}: duplicate id {eid}")
ids.add(eid)
etype = e.get("type")
expected_prefix = ID_PREFIX_BY_TYPE.get(etype)
if expected_prefix and not eid.startswith(expected_prefix):
errors.append(f"line {e['_lineno']}: id {eid} should start with {expected_prefix} for type {etype}")
return ids, errors
def validate_required_fields(entries):
errors = []
for e in entries:
etype = e.get("type")
if etype not in VALID_TYPES:
errors.append(f"line {e['_lineno']}: unknown type {etype}")
continue
required = REQUIRED_FIELDS_BY_TYPE.get(etype, set())
missing = required - set(e.keys())
if missing:
errors.append(f"line {e['_lineno']}: {etype} {e.get('id')} missing fields {sorted(missing)}")
return errors
def validate_references(entries, all_ids):
errors = []
for e in entries:
eid = e.get("id", "?")
for field in REFERENCE_FIELDS:
if field not in e:
continue
val = e[field]
if val is None:
continue
if isinstance(val, str):
refs = [val]
elif isinstance(val, list):
refs = val
else:
errors.append(f"line {e['_lineno']}: {eid}.{field} must be string or list, got {type(val).__name__}")
continue
for ref in refs:
if not ref:
continue
# 只校验看起来像本体 ID 的引用原始字段名phone/code/token跳过
if not is_id_like(ref):
continue
if ref not in all_ids:
errors.append(f"line {e['_lineno']}: {eid}.{field} references unknown id {ref}")
return errors
def validate_evidence(entries):
errors = []
for e in entries:
ev = e.get("evidence")
if ev is None:
errors.append(f"line {e['_lineno']}: {e.get('id')} missing evidence")
continue
if ev not in VALID_EVIDENCE:
errors.append(f"line {e['_lineno']}: {e.get('id')} invalid evidence '{ev}', must be one of {VALID_EVIDENCE}")
return errors
def print_stats(entries):
type_counts = Counter(e.get("type", "?") for e in entries)
evidence_counts = Counter(e.get("evidence", "?") for e in entries)
print("\n=== 本体统计 ===")
print(f"总条目数: {len(entries)}")
print("\n按类型:")
for t in sorted(type_counts):
print(f" {t:10s} {type_counts[t]}")
print("\n按证据强度:")
for ev in sorted(evidence_counts):
print(f" {ev:12s} {evidence_counts[ev]}")
total = len(entries)
if total:
anchored = evidence_counts.get("anchored", 0)
print(f"\nanchored 占比: {anchored}/{total} = {anchored/total*100:.1f}%")
def main():
docs_root = sys.argv[1] if len(sys.argv) > 1 else "docs"
jsonl_path = os.path.join(docs_root, "graph", "ontology.jsonl")
if not os.path.exists(jsonl_path):
print(f"ERROR: {jsonl_path} not found")
sys.exit(1)
entries, errors = load_jsonl(jsonl_path)
if errors:
print("=== JSONL 解析错误 ===")
for err in errors:
print(f" {err}")
sys.exit(1)
all_ids, id_errors = validate_ids(entries)
field_errors = validate_required_fields(entries)
ref_errors = validate_references(entries, all_ids)
ev_errors = validate_evidence(entries)
all_errors = id_errors + field_errors + ref_errors + ev_errors
if all_errors:
print("=== 校验失败 ===")
for err in all_errors:
print(f" {err}")
print(f"\n{len(all_errors)} 个错误")
print_stats(entries)
sys.exit(1)
print("=== 校验通过 ===")
print(f" {jsonl_path}: {len(entries)} 条目,引用完整,证据强度合法")
print_stats(entries)
sys.exit(0)
if __name__ == "__main__":
main()