You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

172 lines
6.7 KiB

2 days ago
# -*- coding: utf-8 -*-
"""生成 A4 响应描述缺口的静态普查报告。"""
import importlib.util
import json
import re
from collections import Counter
from pathlib import Path
ROOT = Path(r"d:\code\crm-backend-matt")
EFFORT = ROOT / ".scratch" / "a4-doc-fields"
E2E = ROOT / ".scratch" / "customer-e2e"
SOURCES = [ROOT / "crm-customer" / "src" / "main" / "java", ROOT / "crm-rule" / "src" / "main" / "java"]
def load_defs():
spec = importlib.util.spec_from_file_location("defs_a4", E2E / "defs_a4.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.DEFS
def load_specimens():
specimens = {}
for name in ("specimens-core.json", "specimens-heavy.json"):
with (E2E / name).open(encoding="utf-8") as source:
specimens.update(json.load(source))
return specimens
def specimen_for(definition, specimens):
path = definition["path"]
candidates = [path]
if "{workspace}" in path:
candidates = [path.replace("{workspace}", workspace) for workspace in ("mine", "overview", "pool")]
for candidate in candidates:
for normalized in (candidate, re.sub(r"\{(id|customerId|memberUserId|taskId)\}", "{id}", candidate)):
found = specimens.get(f"{definition['method']} {normalized}")
if found is not None:
return found
return None
def classify(response):
page = re.search(r"PageResult<([A-Za-z][A-Za-z0-9_]*)>", response)
if page:
return "PageResult<T>", page.group(1)
listed = re.search(r"List<([A-Za-z][A-Za-z0-9_]*)>", response)
if listed:
return "List<T>", listed.group(1)
if response.startswith("null"):
return "空响应", None
if response.startswith("非信封") or response.startswith("文字"):
return "非标准响应", None
if response.startswith("") and "id" in response:
return "原始标量", None
if response.startswith("任务 taskId"):
return "原始标量", None
if response.startswith("BatchResult"):
return "内联结构", "BatchResult"
if response.startswith("CustomerReminderRule{"):
return "内联结构", "CustomerReminderRule"
name = re.match(r"([A-Za-z][A-Za-z0-9_]*)", response)
return ("单个 DTO", name.group(1)) if name else ("非标准响应", None)
def source_index():
index = {}
for source_root in SOURCES:
for path in source_root.rglob("*.java"):
index.setdefault(path.stem, []).append(path)
return index
def definition_lines():
lines = (E2E / "defs_a4.py").read_text(encoding="utf-8").splitlines()
return {
name: number
for number, line in enumerate(lines, 1)
for name in re.findall(r"dict\([^\n]*name='([^']+)'", line)
}
def relative(path):
return path.relative_to(ROOT).as_posix()
def main():
definitions = load_defs()
specimens = load_specimens()
missing = [item for item in definitions if ((specimen_for(item, specimens) or {}).get("response") or {}).get("data") is None]
sources = source_index()
lines = definition_lines()
rows = []
for item in missing:
kind, type_name = classify(item["resp"])
matches = sources.get(type_name, []) if type_name else []
rows.append({"item": item, "kind": kind, "type": type_name, "matches": matches, "line": lines[item["name"]]})
out = [
"# A4 响应字段缺口返回类型普查",
"",
"依据 `.scratch/customer-e2e/defs_a4.py` 与两份 specimens JSON 静态计算;缺口定义为未取得 `response.data` 的端点。源码命中范围为 `crm-customer` 与 `crm-rule` 的 `src/main/java`。",
"",
"## 汇总",
"",
f"- A4 定义端点总数:{len(definitions)}",
f"- 已有 E2E `response.data`、生成器可展开字段表:{len(definitions) - len(rows)}",
f"- 当前缺口:{len(rows)}",
"",
"| 返回形态 | 端点数 |",
"| --- | ---: |",
]
for kind, count in sorted(Counter(row["kind"] for row in rows).items()):
out.append(f"| {kind} | {count} |")
out.extend(["", "## 各类代表端点", ""])
for kind in sorted({row["kind"] for row in rows}):
examples = [row for row in rows if row["kind"] == kind][:2]
out.append(f"- {kind}" + "".join(
f"`{row['item']['method']} {row['item']['path']}`(`defs_a4.py:{row['line']}`)" for row in examples
))
source_candidates = [row for row in rows if row["type"] and row["kind"] not in ("内联结构",)]
unique = [row for row in source_candidates if len(row["matches"]) == 1]
missing_source = [row for row in source_candidates if len(row["matches"]) == 0]
ambiguous = [row for row in source_candidates if len(row["matches"]) > 1]
out.extend([
"",
"## Java 类型名直查结果",
"",
f"- 可按 `resp` 中类型名唯一命中 `.java` 文件:{len(unique)} 条。",
f"- 找不到同名 `.java` 文件:{len(missing_source)} 条。",
f"- 同名文件歧义:{len(ambiguous)} 条。",
"",
])
if missing_source:
out.extend(["### 未命中类型", ""])
for row in missing_source:
item = row["item"]
out.append(f"- `{item['method']} {item['path']}` — `{row['type']}`;`resp`:{item['resp']}")
out.append("")
if ambiguous:
out.extend(["### 歧义类型", ""])
for row in ambiguous:
item = row["item"]
locations = "".join(f"`{relative(path)}`" for path in row["matches"])
out.append(f"- `{item['method']} {item['path']}` — `{row['type']}`:{locations}")
out.append("")
out.extend(["## 逐端点清单", ""])
for number, row in enumerate(rows, 1):
item = row["item"]
out.extend([
f"### {number:02d}. {item['name']}",
"",
f"- 对账键:`{item['method']} {item['path']}`(`defs_a4.py:{row['line']}`)",
f"- 返回描述:`{item['resp']}`",
f"- 分类:{row['kind']}" + (f";候选类型:`{row['type']}`" if row["type"] else ""),
])
if row["matches"]:
out.append("- 源码命中:" + "".join(f"`{relative(path)}`" for path in row["matches"]))
elif row["type"]:
out.append("- 源码命中:无")
out.append("")
(EFFORT / "endpoint-response-survey.md").write_text("\n".join(out), encoding="utf-8", newline="\n")
print(f"defs={len(definitions)} expanded={len(definitions) - len(rows)} missing={len(rows)}")
print(f"source unique={len(unique)} missing={len(missing_source)} ambiguous={len(ambiguous)}")
if __name__ == "__main__":
main()