first commit
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user