Files

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

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import sys
from collections import defaultdict
from pathlib import Path
import yaml
CATEGORY_MAPPING = {
"基本信息": ["basic_info", "基本信息"],
"技术特性": ["technical_features", "technical_characteristics", "技术特性"],
"性能指标": ["performance_metrics", "performance", "性能指标"],
"里程碑意义": ["milestone_significance", "milestones", "里程碑意义"],
"商业信息": ["business_info", "commercial_info", "商业信息"],
"竞争与生态": ["competition_ecosystem", "competition", "竞争与生态"],
"历史沿革": ["history", "历史沿革"],
"市场定位": ["market_positioning", "market", "市场定位"],
}
_SKIP_KEYS = {"_source_file", "uncertain"}
def load_fields_yaml(fields_path):
"""解析 fields.yaml,仅接受 research skill 实际生成的唯一 schema:
fields:
<category>:
- {name: ..., description: ..., detail_level: ...}
...
uncertain: []
只接受这一种格式。不符合的 fields.yaml 会直接报错退出,
而不是以零字段静默通过。
Required 语义(避免校验器空转通过/说谎):
- 若任一字段显式带 `required:` 键 -> 沿用 opt-in 语义
- 否则(detail_level 式、无标记)-> 全部字段视为必填,因为脚本的目的就是完整覆盖。
"""
with fields_path.open(encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
defs = [] # (name, category, required_or_None)
fn = data.get("fields")
if not isinstance(fn, dict):
print(f"[错误] fields.yaml 必须使用 `fields: {{<category>: [{{name, ...}}]}}` 格式;当前为 {type(fn).__name__ if fn is not None else 'None'}。")
sys.exit(1)
for cname, flist in fn.items():
if cname in _SKIP_KEYS:
continue
if not isinstance(flist, list):
print(f"[错误] 分类 `{cname}` 必须映射为字段字典列表;当前为 {type(flist).__name__}。")
sys.exit(1)
for field in flist:
if isinstance(field, dict) and "name" in field:
defs.append((str(field["name"]), str(cname), field.get("required", None)))
else:
print(f"[错误] `{cname}` 下的字段必须是带 `name` 键的字典;当前为 {field!r}。")
sys.exit(1)
if not defs:
print("[错误] fields.yaml 解析到零个字段。请确保至少有一个分类及其字段字典。")
sys.exit(1)
all_fields = {n for n, _, _ in defs}
if any(r is not None for _, _, r in defs):
required_fields = {n for n, _, r in defs if r}
else:
required_fields = set(all_fields)
field_categories = {n: c for n, c, _ in defs}
return all_fields, required_fields, field_categories
def extract_json_fields(data, category_mapping=None):
category_mapping = CATEGORY_MAPPING if category_mapping is None else category_mapping
nested_keys = {k for keys in category_mapping.values() for k in keys}
fields = set()
stack = [(data, True)]
while stack:
obj, is_category_level = stack.pop()
if isinstance(obj, dict):
for k, v in obj.items():
if k in _SKIP_KEYS:
continue
if is_category_level and k in nested_keys:
if isinstance(v, dict):
stack.append((v, True))
continue
fields.add(k)
elif isinstance(obj, list):
stack.extend((item, is_category_level) for item in obj if isinstance(item, dict))
return fields
def validate_json(json_path, all_fields, required_fields, field_categories):
with json_path.open(encoding="utf-8") as f:
data = json.load(f)
json_fields = extract_json_fields(data)
covered = all_fields & json_fields
missing = all_fields - json_fields
extra = json_fields - all_fields
missing_required = missing & required_fields
missing_by_category = defaultdict(list)
for field in missing:
missing_by_category[field_categories.get(field, "未知")].append(field)
return {
"file": json_path.name,
"total_defined": len(all_fields),
"covered": len(covered),
"missing": len(missing),
"extra": len(extra),
"coverage_rate": len(covered) / len(all_fields) * 100 if all_fields else 100,
"missing_required": sorted(missing_required),
"missing_optional": sorted(missing - required_fields),
"missing_by_category": {k: sorted(v) for k, v in missing_by_category.items()},
"extra_fields": sorted(extra),
"valid": len(missing_required) == 0,
}
def print_result(result, verbose=True):
status = "通过" if result["valid"] else "失败"
line = "=" * 60
print(f"\n{line}")
print(f"[{status}] {result['file']}")
print(line)
print(f"覆盖率: {result['coverage_rate']:.1f}% ({result['covered']}/{result['total_defined']})")
if result["missing_required"]:
print(f"\n[错误] 缺少必填字段 ({len(result['missing_required'])}):")
print("\n".join(f" - {f}" for f in result["missing_required"]))
if verbose and result["missing_optional"]:
missing_required = set(result["missing_required"])
print(f"\n[警告] 缺少可选字段 ({len(result['missing_optional'])}):")
for cat in sorted(result["missing_by_category"]):
optional = [f for f in result["missing_by_category"][cat] if f not in missing_required]
if optional:
print(f" [{cat}]: {', '.join(optional)}")
if verbose and result["extra_fields"]:
extra = result["extra_fields"]
print(f"\n[信息] 额外字段 ({len(extra)}):")
print(f" {', '.join(extra[:10])}")
if len(extra) > 10:
print(f" ... 还有 {len(extra) - 10} 个")
def main():
import argparse
parser = argparse.ArgumentParser(description="验证JSON文件是否覆盖fields.yaml中定义的所有字段")
parser.add_argument("--fields", "-f", type=str, help="fields.yaml路径", default="fields.yaml")
parser.add_argument("--json", "-j", type=str, nargs="*", help="要验证的JSON文件路径")
parser.add_argument("--dir", "-d", type=str, help="包含JSON文件的目录", default="results")
parser.add_argument("--quiet", "-q", action="store_true", help="仅显示摘要")
args = parser.parse_args()
fields_path = Path(args.fields)
if not fields_path.exists():
for p in (Path.cwd() / "fields.yaml", Path.cwd().parent / "fields.yaml"):
if p.exists():
fields_path = p
break
if not fields_path.exists():
print(f"[错误] 找不到fields.yaml: {fields_path}")
sys.exit(1)
print(f"字段定义文件: {fields_path}")
all_fields, required_fields, field_categories = load_fields_yaml(fields_path)
print(f"总字段数: {len(all_fields)} (必填: {len(required_fields)}, 可选: {len(all_fields) - len(required_fields)})")
json_files = (
[Path(p) for p in args.json]
if args.json
else sorted(Path(args.dir).glob("*.json")) if Path(args.dir).exists() else []
)
if not json_files:
print("[警告] 未找到JSON文件")
sys.exit(0)
results = []
for json_path in json_files:
if not json_path.exists():
print(f"[警告] 文件不存在: {json_path}")
continue
result = validate_json(json_path, all_fields, required_fields, field_categories)
results.append(result)
print_result(result, verbose=not args.quiet)
line = "=" * 60
print(f"\n{line}")
print("汇总")
print(line)
passed = sum(1 for r in results if r["valid"])
avg_coverage = sum(r["coverage_rate"] for r in results) / len(results) if results else 0
print(f"验证通过: {passed}/{len(results)}")
print(f"平均覆盖率: {avg_coverage:.1f}%")
if passed < len(results):
sys.exit(1)
if __name__ == "__main__":
main()