-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrender_report.py
More file actions
160 lines (134 loc) · 5.52 KB
/
Copy pathrender_report.py
File metadata and controls
160 lines (134 loc) · 5.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
"""Validate and save a concise six-section Skill deconstruction report."""
from __future__ import annotations
import argparse
import os
import re
import sys
from pathlib import Path
REQUIRED_HEADINGS = [
"## 1. 这个 Skill 最后要交付什么",
"## 2. 它具体做了哪些事情",
"## 3. 为什么要按这个顺序做",
"## 4. 用户、AI、脚本和外部工具各负责什么",
"## 5. 这些规则是在避免什么问题",
"## 6. 哪些做法可以用到其他任务中",
]
FORBIDDEN_PHRASES = (
"第一部分",
"第二部分",
"主 SKILL.md 全文与批注",
"相关文件与工具简介",
"证据范围",
"关键证据",
"发现的问题与改进建议",
"原文明确写出",
"根据文件推断",
"这个 Skill 的道",
"它的“道”",
'它的"道"',
"底层哲学",
)
EVIDENCE_ID_RE = re.compile(r"(?<![A-Za-z0-9_])E\d+(?![A-Za-z0-9_])")
SOURCE_LINE_RE = re.compile(
r"(?:SKILL\.md|[A-Za-z0-9_.-]+\.(?:md|py|ya?ml|json|ts|js))"
r"(?::\d+|\s+L\d+)",
re.IGNORECASE,
)
H2_RE = re.compile(r"^## (?!#).+$", re.MULTILINE)
class ReportError(ValueError):
"""Raised when a report violates the concise output contract."""
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Validate and save a concise six-section Skill report."
)
parser.add_argument("--analysis", required=True, type=Path)
parser.add_argument("--title", required=True)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--max-chars", type=int, default=4500)
return parser.parse_args()
def read_utf8(path: Path) -> str:
if not path.is_file():
raise ReportError(f"报告正文不存在或不是文件:{path}")
try:
return path.read_bytes().decode("utf-8")
except UnicodeDecodeError as exc:
raise ReportError(f"报告正文必须使用 UTF-8 编码:{path}") from exc
def validate_analysis(analysis: str, max_chars: int) -> None:
if max_chars < 500:
raise ReportError("max-chars 不能小于 500。")
if not analysis.strip():
raise ReportError("报告正文不能为空。")
if len(analysis) > max_chars:
raise ReportError(
f"报告正文有 {len(analysis)} 个字符,超过上限 {max_chars};请先压缩。"
)
actual_h2 = H2_RE.findall(analysis)
if actual_h2 != REQUIRED_HEADINGS:
raise ReportError("报告必须且只能包含规定的六个二级章节,并保持正确顺序。")
positions = [analysis.index(heading) for heading in REQUIRED_HEADINGS]
for index, heading in enumerate(REQUIRED_HEADINGS):
start = positions[index] + len(heading)
end = positions[index + 1] if index + 1 < len(positions) else len(analysis)
if not analysis[start:end].strip():
raise ReportError(f"章节不能为空:{heading}")
found = [phrase for phrase in FORBIDDEN_PHRASES if phrase in analysis]
if found:
raise ReportError(f"报告含有已删除或不使用的内容:{', '.join(found)}。")
if EVIDENCE_ID_RE.search(analysis):
raise ReportError("报告不要列证据编号。")
if SOURCE_LINE_RE.search(analysis):
raise ReportError("报告不要列源码文件与行号。")
def validate_output_path(output: Path) -> Path:
reports_dir = Path.cwd() / "reports"
if reports_dir.exists():
if reports_dir.is_symlink():
raise ReportError("reports/ 不能是符号链接。")
if not reports_dir.is_dir():
raise ReportError("reports/ 已存在,但它不是文件夹。")
resolved_reports = reports_dir.resolve(strict=False)
resolved_output = output.resolve(strict=False)
if resolved_output.parent != resolved_reports:
raise ReportError("输出文件必须直接放在当前工作区的 reports/ 文件夹中。")
if output.is_symlink():
raise ReportError("输出文件不能是符号链接。")
if resolved_output.exists():
raise ReportError(
f"输出文件已存在,不会覆盖:{resolved_output}。请增加版本号或时间戳。"
)
return resolved_output
def write_report(output: Path, content: str) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
created = False
try:
with output.open("xb") as handle:
created = True
handle.write(content.encode("utf-8"))
handle.flush()
os.fsync(handle.fileno())
except FileExistsError as exc:
raise ReportError(
f"输出文件已存在,不会覆盖:{output}。请增加版本号或时间戳。"
) from exc
except Exception:
if created and output.is_file() and not output.is_symlink():
output.unlink()
raise
def build_report(title: str, analysis: str) -> str:
return f"# {title} 拆解报告\n\n{analysis.strip()}\n"
def main() -> int:
args = parse_args()
try:
if not args.title.strip() or "\n" in args.title or "\r" in args.title:
raise ReportError("title 必须是一行非空文字。")
analysis = read_utf8(args.analysis)
validate_analysis(analysis, args.max_chars)
output = validate_output_path(args.output)
write_report(output, build_report(args.title.strip(), analysis))
print(output)
return 0
except (OSError, ReportError) as exc:
print(f"错误:{exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())