first commit

This commit is contained in:
xiang
2026-07-25 23:45:09 +08:00
commit c7bd957e6f
47 changed files with 3870 additions and 0 deletions
@@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""安全初始化 Vibe Coding 的 L1 或 L2 文档目录。"""
from __future__ import annotations
import argparse
import datetime as dt
import re
import shutil
import sys
import tempfile
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parent.parent
TEMPLATES = SKILL_ROOT / "assets" / "templates"
INVALID_NAME = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
def safe_name(value: str) -> str:
cleaned = INVALID_NAME.sub("_", value).strip().rstrip(".")
if not cleaned:
raise ValueError("名称不能为空或只包含非法字符")
return cleaned
def render(template_name: str, replacements: dict[str, str]) -> str:
text = (TEMPLATES / template_name).read_text(encoding="utf-8")
for key, value in replacements.items():
text = text.replace("{{" + key + "}}", value)
return text
def write_new(path: Path, content: str) -> None:
if path.exists():
raise FileExistsError(f"拒绝覆盖已有文件:{path}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def parse_subtask(raw: str) -> tuple[str, str]:
if ":" not in raw:
raise ValueError(f"子任务格式应为 ST-001:名称,实际为:{raw}")
number, name = raw.split(":", 1)
number = number.strip().upper()
if not re.fullmatch(r"ST-\d{3}", number):
raise ValueError(f"子任务编号格式错误:{number}")
return number, safe_name(name)
def build_l1(args: argparse.Namespace) -> Path:
now = dt.datetime.now().astimezone().isoformat(timespec="seconds")
replacements = {
"需求名称": args.name,
"编号": args.requirement_id or "待确认",
"状态": "待确认",
"时间": now,
}
if args.subtask_dir:
target_dir = Path(args.subtask_dir).resolve()
if not target_dir.is_dir():
raise FileNotFoundError(f"子任务目录不存在:{target_dir}")
write_new(
target_dir / "01.精简变更日志.md",
render("L1-精简变更日志模板.md", replacements),
)
return target_dir
management_root = Path(args.management_root).resolve()
management_root.mkdir(parents=True, exist_ok=True)
pm_id = safe_name(args.requirement_id or "待确认")
target = management_root / f"PM-{pm_id}-{safe_name(args.name)}"
if target.exists():
raise FileExistsError(f"目标目录已存在:{target}")
temporary = Path(tempfile.mkdtemp(prefix=".vibe-init-", dir=management_root))
try:
write_new(
temporary / "01.精简变更日志.md",
render("L1-精简变更日志模板.md", replacements),
)
temporary.replace(target)
except Exception:
shutil.rmtree(temporary, ignore_errors=True)
raise
return target
def build_l2(args: argparse.Namespace) -> Path:
management_root = Path(args.management_root).resolve()
management_root.mkdir(parents=True, exist_ok=True)
pm_id = safe_name(args.pm_id or "待确认")
demand_name = safe_name(args.name)
target = management_root / f"PM-{pm_id}-{demand_name}"
if target.exists():
raise FileExistsError(f"目标目录已存在:{target}")
subtasks = [parse_subtask(item) for item in args.subtask]
if not subtasks:
raise ValueError("L2 至少需要一个 --subtask ST-001:名称")
numbers = [number for number, _ in subtasks]
if len(numbers) != len(set(numbers)):
raise ValueError("子任务编号不能重复")
now = dt.datetime.now().astimezone().isoformat(timespec="seconds")
common = {
"PM编号": pm_id,
"需求名称": demand_name,
"名称": demand_name,
"状态": "需求分析中",
"阶段": "需求分析",
"时间": now,
}
temporary = Path(tempfile.mkdtemp(prefix=".vibe-init-", dir=management_root))
try:
source_version = safe_name(args.source_version)
(temporary / "source" / source_version).mkdir(parents=True)
(temporary / "code").mkdir(parents=True)
(temporary / "tasks").mkdir(parents=True)
overview = render("大需求总览模板.md", common)
default_row = (
"| ST-001 | {{模块}} | 需求分析 | 需求分析中 | {{依赖}} | "
"[查看](./tasks/ST-001-模块/01.需求分析.md) |"
)
rows = "\n".join(
f"| {number} | {name} | 需求分析 | 未开始 | 待梳理 | "
f"[查看](./tasks/{number}-{name}/01.需求分析.md) |"
for number, name in subtasks
)
overview = overview.replace(default_row, rows)
write_new(temporary / "README.md", overview)
write_new(temporary / "STATUS.md", render("任务状态模板.md", common))
write_new(
temporary / "技术侧需求分析.md",
render("技术侧需求分析模板.md", common),
)
source_index = render("原始需求版本索引模板.md", common)
source_index = source_index.replace("| v1 |", f"| {source_version} |")
source_index = source_index.replace("./v1/", f"./{source_version}/")
write_new(temporary / "source" / "README.md", source_index)
write_new(
temporary / "code" / "MANIFEST.md",
render("代码副本清单模板.md", common),
)
phase_templates = [
("01-需求分析模板.md", "01.需求分析.md"),
("02-技术实现方案模板.md", "02.技术实现方案.md"),
("03-冒烟与逻辑验证模板.md", "03.冒烟与逻辑验证.md"),
("04-技术实现记录模板.md", "04.技术实现记录.md"),
("05-代码Review报告模板.md", "05.代码Review报告.md"),
("06-验收与交付报告模板.md", "06.验收与交付报告.md"),
]
for number, name in subtasks:
task_dir = temporary / "tasks" / f"{number}-{name}"
replacements = dict(common)
replacements.update(
{
"ST编号": number,
"子任务名称": name,
"PM编号及链接": f"[{pm_id}](../../README.md)",
"链接": "../../README.md",
}
)
for template_name, output_name in phase_templates:
write_new(
task_dir / output_name,
render(template_name, replacements),
)
temporary.replace(target)
except Exception:
shutil.rmtree(temporary, ignore_errors=True)
raise
return target
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="初始化 Vibe Coding L1/L2 文档,拒绝覆盖已有目标。",
)
subparsers = parser.add_subparsers(dest="level", required=True)
l1 = subparsers.add_parser("l1", help="初始化一份 L1 精简变更日志")
l1.add_argument("--name", required=True, help="需求名称")
l1.add_argument("--requirement-id", help="需求编号;未提供时使用“待确认”")
l1_target = l1.add_mutually_exclusive_group(required=True)
l1_target.add_argument("--management-root", help="独立需求的 management 目录")
l1_target.add_argument("--subtask-dir", help="既有大需求下的子任务目录")
l2 = subparsers.add_parser("l2", help="初始化完整 L2 PM 目录")
l2.add_argument("--management-root", required=True, help="management 目录")
l2.add_argument("--name", required=True, help="大需求名称")
l2.add_argument("--pm-id", help="PM 编号;未提供时使用“待确认”")
l2.add_argument("--source-version", default="v1", help="初始原始需求版本")
l2.add_argument(
"--subtask",
action="append",
default=[],
help="子任务,格式 ST-001:名称;可重复",
)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
try:
target = build_l1(args) if args.level == "l1" else build_l2(args)
except (OSError, ValueError) as exc:
print(f"初始化失败:{exc}", file=sys.stderr)
return 1
print(f"初始化完成:{target}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""校验 Vibe Coding 文档结构、链接、占位符和功能追溯。"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
PLACEHOLDER = re.compile(r"\{\{[^{}\n]+\}\}")
MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
FUNCTION_ID = re.compile(r"\bF-\d{3}\b")
OLD_NAMES = {
"03.冒烟自测用例.md",
"06.逻辑验证报告.md",
"07.验收与交付报告.md",
}
class Report:
def __init__(self) -> None:
self.errors: list[str] = []
self.warnings: list[str] = []
def error(self, message: str) -> None:
self.errors.append(message)
def warning(self, message: str) -> None:
self.warnings.append(message)
def read_text(path: Path, report: Report) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
report.error(f"文件不是有效 UTF-8{path}")
except OSError as exc:
report.error(f"无法读取文件:{path}{exc}")
return ""
def check_links(path: Path, text: str, report: Report) -> None:
for match in MARKDOWN_LINK.finditer(text):
target = match.group(1).strip()
if (
not target
or target.startswith(("#", "http://", "https://", "mailto:"))
or "{{" in target
):
continue
relative = target.split("#", 1)[0]
if not (path.parent / relative).resolve().exists():
report.error(f"失效链接:{path} -> {target}")
def required_root_files(root: Path) -> list[Path]:
return [
root / "README.md",
root / "STATUS.md",
root / "技术侧需求分析.md",
root / "source" / "README.md",
root / "code" / "MANIFEST.md",
]
def task_directories(root: Path) -> list[Path]:
tasks_root = root / "tasks"
if not tasks_root.is_dir():
return []
return sorted(path for path in tasks_root.iterdir() if path.is_dir())
def required_task_names(phase: str) -> list[str]:
names = ["01.需求分析.md"]
if phase in {"g2", "g3", "archive"}:
names += ["02.技术实现方案.md", "03.冒烟与逻辑验证.md"]
if phase in {"g3", "archive"}:
names += ["04.技术实现记录.md", "05.代码Review报告.md"]
if phase == "archive":
names += ["06.验收与交付报告.md"]
return names
def check_traceability(task: Path, report: Report) -> None:
requirement = read_text(task / "01.需求分析.md", report)
design = read_text(task / "02.技术实现方案.md", report)
validation = read_text(task / "03.冒烟与逻辑验证.md", report)
required_ids = set(FUNCTION_ID.findall(requirement))
if not required_ids:
report.warning(f"未在需求分析中发现功能编号:{task}")
return
for function_id in sorted(required_ids):
if function_id not in design:
report.error(f"{task.name}{function_id} 未出现在技术方案中")
if function_id not in validation:
report.error(f"{task.name}{function_id} 未出现在验证文档中")
def validate_l1(
root: Path,
allow_placeholders: bool,
report: Report,
) -> None:
document = root / "01.精简变更日志.md"
if not document.is_file():
report.error(f"缺少 L1 文档:{document}")
return
text = read_text(document, report)
if not allow_placeholders and PLACEHOLDER.search(text):
report.error(f"存在未填写占位符:{document}")
check_links(document, text, report)
def validate_l2(
root: Path,
phase: str,
allow_placeholders: bool,
report: Report,
) -> None:
required = required_root_files(root)
tasks = task_directories(root)
if not tasks:
report.error(f"未发现子任务目录:{root / 'tasks'}")
for task in tasks:
for name in required_task_names(phase):
required.append(task / name)
for child in task.iterdir():
if child.name in OLD_NAMES:
report.error(f"发现旧版文档名称:{child}")
if phase == "archive":
required.append(root / "归档记录.md")
existing_required: list[Path] = []
for path in required:
if not path.is_file():
report.error(f"缺少必需文件:{path}")
else:
existing_required.append(path)
status = root / "STATUS.md"
if status.is_file():
status_text = read_text(status, report)
for heading in ("原始需求版本", "确认文档及版本", "代码版本标识"):
if heading not in status_text:
report.error(f"STATUS.md 缺少门禁绑定字段:{heading}")
for path in existing_required:
text = read_text(path, report)
if not allow_placeholders and PLACEHOLDER.search(text):
report.error(f"存在未填写占位符:{path}")
check_links(path, text, report)
if phase in {"g2", "g3", "archive"}:
for task in tasks:
if all((task / name).is_file() for name in required_task_names("g2")):
check_traceability(task, report)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="校验 Vibe Coding 的 L1/L2 文档完整性。",
)
parser.add_argument("path", help="L1 所在目录或 L2 PM 目录")
parser.add_argument(
"--phase",
choices=["l1", "structure", "g1", "g2", "g3", "archive"],
required=True,
help="目标校验阶段",
)
parser.add_argument(
"--allow-placeholders",
action="store_true",
help="允许模板占位符,适合刚初始化后的结构检查",
)
return parser
def main() -> int:
args = build_parser().parse_args()
root = Path(args.path).resolve()
if not root.is_dir():
print(f"校验失败:目录不存在:{root}", file=sys.stderr)
return 1
report = Report()
if args.phase == "l1":
validate_l1(root, args.allow_placeholders, report)
else:
validate_l2(root, args.phase, args.allow_placeholders, report)
for warning in report.warnings:
print(f"警告:{warning}")
for error in report.errors:
print(f"错误:{error}", file=sys.stderr)
if report.errors:
print(f"校验失败:{len(report.errors)} 个错误,{len(report.warnings)} 个警告")
return 1
print(f"校验通过:0 个错误,{len(report.warnings)} 个警告")
return 0
if __name__ == "__main__":
raise SystemExit(main())