Files
agent-radar/scripts/new_project.py
T

175 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
"""从 schema 生成一个项目的空白档案骨架,并自动预填可机器获取的字段。
用法
----
python3 scripts/new_project.py --repo Devin-AXIS/iPolloWork
python3 scripts/new_project.py --repo owner/name --id custom-id --status watch
生成后需要人工补完的内容(脚本不猜这些):
identity / business / scenario / tech / risk / verdict
其中 **risk.claim_vs_reality 是必填**,它承载"宣称 vs 实测"的落差分析,
是整套档案里最不能空着的字段。
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _common import ( # noqa: E402
DATA,
PROJECTS,
SCHEMA,
classify_license,
gh_get,
load_yaml,
today,
)
LEAF_KEYS = {"type", "required", "desc", "enum", "auto", "note"}
def render(node, indent=0, out=None, prefix="", idx=None):
"""把 schema 的 fields 树渲染成带注释的 YAML 文本。
同时返回 {字段路径: 行号} 索引。回填值必须走这个索引按**路径**定位,
不能靠硬编码缩进 —— schema 一旦加减层级,缩进常量就会悄悄错位,
而且像 `name` 这种同名不同层的字段(项目名 vs 许可证名)会互相覆盖。
"""
out = out if out is not None else []
idx = idx if idx is not None else {}
pad = " " * indent
for key, spec in node.items():
if not isinstance(spec, dict):
continue
path = f"{prefix}{key}"
if "type" in spec: # 叶子字段
default = {
"str": '""', "int": "0", "bool": "false",
"list": "[]", "dict": "{}",
}.get(spec["type"], '""')
comment = ""
if spec.get("enum"):
# fields 里的 enum 是 enums 段的键名引用(字符串),不是字面量数组;
# 直接 join 会把 "vendor_type" 拆成 v|e|n|d|o|r|_|t|y|p|e
e = spec["enum"]
comment = f" # {'|'.join(e) if isinstance(e, list) else e}"
elif spec.get("desc"):
d = re.sub(r"\s+", " ", spec["desc"].replace("\n", " "))
comment = f" # {d[:60]}"
idx[path] = len(out)
out.append(f"{pad}{key}: {default}{comment}")
elif spec and not (set(spec) & LEAF_KEYS): # 嵌套 section
out.append(f"{pad}{key}:")
render(spec, indent + 2, out, path + ".", idx)
return out, idx
def slug(repo: str) -> str:
name = repo.rstrip("/").split("/")[-1]
name = re.sub(r"\.git$", "", name)
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--repo", required=True, help="GitHub 仓库 owner/name")
ap.add_argument("--id", help="档案 id,默认由仓库名生成")
ap.add_argument("--status", default="active", choices=["active", "watch", "archived"])
args = ap.parse_args()
pid = args.id or slug(args.repo)
ypath = PROJECTS / f"{pid}.yaml"
if ypath.exists():
print(f"档案已存在:{ypath}")
return 1
print(f"==> 生成档案 {pid} <- {args.repo}")
# ---- 抓可自动获取的部分 ----
info = gh_get(f"repos/{args.repo}") or {}
lic = info.get("license") or {}
spdx = lic.get("spdx_id") or ""
if spdx == "NOASSERTION":
spdx = "NOASSERTION"
cls = classify_license(spdx, lic.get("name", ""))
print(f" 许可证 spdx={spdx or '(无)'} → {cls['level']} / 可见性 {cls['visibility']}")
schema = load_yaml(SCHEMA / "project.schema.yaml")
lines, idx = render(schema.get("fields", {}))
def setval(path: str, val) -> None:
"""按字段路径回填值,保留该行原有的缩进与键名。"""
i = idx.get(path)
if i is None:
print(f" [warn] schema 中无字段 {path},跳过回填")
return
line = lines[i]
indent = " " * (len(line) - len(line.lstrip()))
key = path.split(".")[-1]
lines[i] = f"{indent}{key}: {val}"
setval("id", pid)
setval("name", f'"{info.get("name", "")}"')
setval("repo", f'"{args.repo}"')
setval("homepage", f'"{info.get("homepage") or ""}"')
setval("added_at", f'"{today()}"')
setval("status", args.status)
if info:
setval("identity.tagline", f'"{info.get("description") or ""}"')
setval("identity.first_seen", f'"{(info.get("created_at") or "")[:10]}"')
setval("business.license.spdx", f'"{spdx or "NOASSERTION"}"')
setval("business.license.name", f'"{lic.get("name") or "自定义许可"}"')
setval("business.license.openness", cls["level"])
setval("business.license.redistributable", "true" if cls["redistributable"] else "false")
setval("mirror.gitea_repo", f'"gitinbox/{info.get("name") or pid}"')
setval("mirror.visibility", cls["visibility"])
setval("mirror.upstream", f'"https://github.com/{args.repo}"')
text = "\n".join(lines)
header = (
f"# {info.get('name') or pid} · 项目档案\n"
f"# 生成于 {today()} · 由 scripts/new_project.py 从 schema 生成\n"
"# 人工补完重点:business / scenario / tech / risk.claim_vs_reality / verdict\n"
"# 字段规范见 schema/project.schema.yaml\n"
"\n"
)
PROJECTS.mkdir(parents=True, exist_ok=True)
ypath.write_text(header + text + "\n", encoding="utf-8")
print(f" 已写入 {ypath.relative_to(PROJECTS.parent.parent)}")
# ---- 登记到 registry ----
reg_path = DATA / "registry.yaml"
reg_text = reg_path.read_text(encoding="utf-8")
if f"id: {pid}" not in reg_text:
entry = (
f" - id: {pid}\n"
f" repo: {args.repo}\n"
f" status: {args.status}\n"
f" added_at: {today()}\n"
f" license_level: {cls['level']}"
+ (" # source-available,镜像私有\n" if cls["level"] == "L3" else "\n")
+ f" mirror_visibility: {cls['visibility']}\n"
)
reg_text = reg_text.rstrip("\n") + "\n" + entry
reg_path.write_text(reg_text, encoding="utf-8")
print(f" 已登记到 data/registry.yaml")
print(f"\n下一步:\n 1. 补完档案(重点 risk.claim_vs_reality)\n"
f" 2. python3 scripts/fetch_stats.py --id {pid}\n"
f" 3. bash scripts/mirror_sync.sh --id {pid}")
return 0
if __name__ == "__main__":
raise SystemExit(main())