@hawk2048/oh-my-dsh 0.2.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -21
- package/README.md +246 -235
- package/README.zh.md +246 -235
- package/agent.cordis.yml +288 -0
- package/commands/omd-commands.mjs +150 -141
- package/cordis.patch.yml +20 -20
- package/index.js +8 -8
- package/install.ps1 +92 -0
- package/install.sh +81 -0
- package/package.json +46 -36
- package/preset.yml +2 -0
- package/skills/omd-ai-slop-cleaner/SKILL.md +43 -43
- package/skills/omd-autopilot/SKILL.md +51 -51
- package/skills/omd-autoresearch/CHANGELOG.md +46 -0
- package/skills/omd-autoresearch/SKILL.md +117 -24
- package/skills/omd-autoresearch/references/eval-prompt.md +79 -0
- package/skills/omd-autoresearch/references/report-rules.md +121 -0
- package/skills/omd-autoresearch/references/report-style.md +228 -0
- package/skills/omd-autoresearch/references/report_lint.py +466 -0
- package/skills/omd-autoresearch/references/rubric.md +89 -0
- package/skills/omd-autoresearch/references/skill-survey.md +158 -0
- package/skills/omd-cancel/SKILL.md +36 -36
- package/skills/omd-debug/SKILL.md +35 -35
- package/skills/omd-deep-analyze/SKILL.md +36 -36
- package/skills/omd-deep-interview/SKILL.md +68 -68
- package/skills/omd-deepsearch/SKILL.md +36 -36
- package/skills/omd-doctor/SKILL.md +39 -39
- package/skills/omd-execute/SKILL.md +41 -41
- package/skills/omd-external-context/SKILL.md +36 -36
- package/skills/omd-help/SKILL.md +35 -35
- package/skills/omd-hud/SKILL.md +80 -79
- package/skills/omd-minimal-code-discipline/SKILL.md +36 -36
- package/skills/omd-plan/SKILL.md +51 -51
- package/skills/omd-pyramid/SKILL.md +121 -0
- package/skills/omd-ralph/SKILL.md +62 -62
- package/skills/omd-ralplan/SKILL.md +45 -45
- package/skills/omd-release/SKILL.md +36 -36
- package/skills/omd-remember/SKILL.md +35 -35
- package/skills/omd-research/SKILL.md +40 -38
- package/skills/omd-review/SKILL.md +47 -45
- package/skills/omd-skillify/SKILL.md +36 -36
- package/skills/omd-tdd/SKILL.md +37 -37
- package/skills/omd-team/SKILL.md +96 -96
- package/skills/omd-ultragoal/SKILL.md +46 -46
- package/skills/omd-ultrathink/SKILL.md +39 -39
- package/skills/omd-verify/SKILL.md +44 -44
- package/skills/omd-visual-verdict/SKILL.md +36 -36
- package/skills/omd-wiki/SKILL.md +36 -36
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
report_lint.py — 报告文字体检器(omd-autoresearch 附带的机器闸门)
|
|
5
|
+
|
|
6
|
+
只查**下限**:水词、AI 腔、句式骨架、密度、篇幅。
|
|
7
|
+
不判断观点对不对、逻辑通不通、数据真不真——那些交给评估子代理与人。
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python report_lint.py 报告.md # 整篇
|
|
11
|
+
python report_lint.py 节.md --scale 0.6 --quiet # 节级(短文本放宽密度阈值)
|
|
12
|
+
python report_lint.py 报告.md --json # 机器可读
|
|
13
|
+
|
|
14
|
+
退出码:0 = 通过(可含 warning);1 = 有「判死」项。
|
|
15
|
+
|
|
16
|
+
阈值来源:ruanyf/document-style-guide(句长/段长/标题层级)、
|
|
17
|
+
MrGeDiao/shuorenhua hard_metrics(层次化判定 + 误杀防护思路)、
|
|
18
|
+
blader/humanizer(结构信号优先于词表)。
|
|
19
|
+
阈值未经本地语料标定,一律按「判死 / 报警」两档处置:报警只报数,不判负。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------- 判死项
|
|
28
|
+
|
|
29
|
+
JUDGE_BANNED = {
|
|
30
|
+
# 用户点名禁用,权重最高。逗号可有可无:中文里「不是A而是B」常常不加逗号,
|
|
31
|
+
# 早期版本强制标点,导致这类变体全部漏检。
|
|
32
|
+
"对偶转折句": [
|
|
33
|
+
r"不是[^。,;!?\n]{2,24}[,,]?\s*而是",
|
|
34
|
+
r"不是[^。,;!?\n]{2,24}[,,]?\s*是(?!的)", # 省略「而」的变体:不是需求问题,是回本期问题
|
|
35
|
+
r"并非[^。,;!?\n]{2,24}[,,]?\s*而是",
|
|
36
|
+
r"与其说[^。,;!?\n]{2,24}[,,]?\s*不如",
|
|
37
|
+
r"不在于[^。,;!?\n]{2,24}[,,]?\s*而在于",
|
|
38
|
+
r"不(仅仅|只是|单)[^。,;!?\n]{2,24}[,,]?\s*更是",
|
|
39
|
+
r"真正的[^。,;!?\n]{0,12}不是[^。,;!?\n]{2,20}[,,]?\s*(而)?是",
|
|
40
|
+
],
|
|
41
|
+
"自我更正元叙述": [
|
|
42
|
+
r"前面(说|提|写)错",
|
|
43
|
+
r"之前(说|提|写)错",
|
|
44
|
+
r"前面(提到|说|提|写|给)的[^。,\n]{0,24}(有误|不对|错了|有问题|是错的|站不住)",
|
|
45
|
+
r"之前(提到|说|提|写|给)的[^。,\n]{0,24}(有误|不对|错了|有问题|是错的)",
|
|
46
|
+
r"我(们)?(前面|刚才|上一条|上面)(说|提|写)的[^。\n]{0,12}(不对|有误|错了)",
|
|
47
|
+
r"(纠正|更正|修正|勘误)一下",
|
|
48
|
+
r"说错了",
|
|
49
|
+
r"(正确|准确)的说法(应该)?是",
|
|
50
|
+
r"重新说一遍",
|
|
51
|
+
r"前面(那个|这|的)?(说法|结论|判断)[^。\n]{0,12}(不成立|要改|得改)",
|
|
52
|
+
],
|
|
53
|
+
"生造黑话": [
|
|
54
|
+
r"钉死", r"焊死", r"锁死", r"打死", r"拉齐", r"对齐颗粒度",
|
|
55
|
+
r"抓手", r"闭环", r"赋能", r"沉淀", r"打通", r"水位", r"心智",
|
|
56
|
+
r"体感", r"链路", r"势能", r"降维打击", r"组合拳", r"颗粒度",
|
|
57
|
+
r"顶层设计", r"兜底", r"压实", r"收口", r"砍一刀", r"补一刀",
|
|
58
|
+
# 比喻化生造:拿伤口、搏斗、烹饪比喻软件问题,正常人不会这么说
|
|
59
|
+
r"小疤", r"打穿", r"啃下(了)?", r"拧巴", r"裸写", r"裸奔",
|
|
60
|
+
r"缝补", r"硬掰", r"贴脸", r"焊在",
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# 工程词外泄:工具输出与流程内部词,不该出现在给读者看的正文里。
|
|
65
|
+
# 判定分两档——纯行话直接判死,权衡用词只提示(对外报告可换,内部文档随意)。
|
|
66
|
+
TOOL_LEAK_JUDGE = [r"报警项", r"判死", r"判负", r"落盘", r"跑通", r"卡闸", r"fail\s*掉", r"打了个补丁"]
|
|
67
|
+
TOOL_LEAK_WARN = [r"闸门", r"拆轮", r"打标", r"跑一遍", r"收口"]
|
|
68
|
+
|
|
69
|
+
# 比喻与借代:拿别的东西指代报告里的概念。对外报告里直接说做了什么。
|
|
70
|
+
# 机器判比喻并不可靠,这里只收高频、且几乎不会作字面用的几个,命中只提示。
|
|
71
|
+
METAPHOR_WARN = [r"骨架", r"双轴", r"全家桶", r"台账", r"三角验证", r"抓手",
|
|
72
|
+
r"闭环", r"链路", r"小疤", r"打穿", r"地基", r"桥梁",
|
|
73
|
+
r"钥匙", r"落地生根", r"痛点"]
|
|
74
|
+
|
|
75
|
+
# 误杀防护:命中上下文白名单则计数作废(同音/同形但语义正常的用法)
|
|
76
|
+
FALSE_POSITIVE_GUARD = [
|
|
77
|
+
r"闭环(控制|反馈|系统)", r"协同编辑", r"消防联动", r"用户洞察",
|
|
78
|
+
r"颗粒度(测试|分析|采样)", r"链路(层|协议)", r"赋能(函数|接口)",
|
|
79
|
+
# 字面义:这些词在行业报告里是正常表达,不是黑话
|
|
80
|
+
r"(地下|河道|水库|海域|上游|中游)水位", r"水位[^,。\n]{0,8}(米|较|下降|上升|监测|站|线)",
|
|
81
|
+
r"体感(温度|气温|温度计)", r"资金沉淀", r"化学沉淀", r"泥沙沉淀",
|
|
82
|
+
r"沉淀(物|池|法|剂)", r"打通(最后一公里|隧道|电话)",
|
|
83
|
+
r"心智(模型|发育|障碍)", r"势能(转化|守恒|势能面)",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------- 提示项
|
|
87
|
+
|
|
88
|
+
WARN_TICS = {
|
|
89
|
+
"空转连接": ["值得注意的是", "值得一提的是", "需要指出的是", "不可否认的是",
|
|
90
|
+
"不难发现", "不容忽视", "众所周知", "由此可见", "综上所述",
|
|
91
|
+
"总而言之", "总的来说", "总体来看", "简而言之", "归根结底",
|
|
92
|
+
"不言而喻", "换句话说", "也就是说", "具体来说", "更重要的是"],
|
|
93
|
+
"开场套话": ["在当今", "随着.*的(不断|快速)(发展|推进)", "在这个.*的时代",
|
|
94
|
+
"让我们一起来", "接下来我将", "深入探讨", "不得不说", "诚然"],
|
|
95
|
+
"伪深刻": ["本质上是", "核心在于", "关键在于", "真正的问题在于",
|
|
96
|
+
"从某种意义上", "某种程度上", "可以说", "这背后的逻辑",
|
|
97
|
+
"标志着.*(新时代|新阶段)", "范式转移"],
|
|
98
|
+
"拔高渲染": ["意义深远", "意义非凡", "前所未有", "史无前例", "毋庸置疑",
|
|
99
|
+
"至关重要", "举足轻重", "令人瞩目", "值得深思", "发人深省",
|
|
100
|
+
"发挥着关键作用", "颠覆性"],
|
|
101
|
+
"客服腔": ["让我们一起", "我们可以看到", "希望.*对(你|您)有帮助",
|
|
102
|
+
"如果(你|您)(还)?有(任何)?(其他)?(问题|疑问)", "祝(你|您).*愉快",
|
|
103
|
+
"好问题", "你说得很对", "让我来为你", "要不要我"],
|
|
104
|
+
"正能量收尾": ["让我们拭目以待", "未来可期", "积极拥抱", "只有.*才能.*未来"],
|
|
105
|
+
"无源引用": ["研究表明", "数据显示", "有专家指出", "业内人士认为",
|
|
106
|
+
"据报道", "观察人士", "有分析认为"],
|
|
107
|
+
"程度副词堆砌": ["非常(重要|关键|显著)", "极大(地)?(提升|改善)",
|
|
108
|
+
"显著(地)?(提升|改善)", "全方位", "深层次", "系统性地"],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
NOMINALIZATION = [r"(进行|实现|完成|开展|起到|具有)(了)?(一)?(个|次)?[^。,\n]{0,6}(优化|提升|改进|调整|分析|管理|建设|部署)"]
|
|
112
|
+
|
|
113
|
+
# 阈值(--scale 只作用于 per_1000 类密度)
|
|
114
|
+
THRESHOLDS = {
|
|
115
|
+
"sentence_hard": 60, # 单句 > 60 字:判死
|
|
116
|
+
"sentence_warn": 40, # 单句 40–60 字:报警(ruanyf:>40 不可接受)
|
|
117
|
+
"para_hard": 300, # 单段 > 300 字:判死(≈7 行 × 43 字,行宽按手机估)
|
|
118
|
+
"para_warn": 200, # 单段 200–300:报警
|
|
119
|
+
"dash_para_hard": 4, # 单段 —— 数 ≥4:判死
|
|
120
|
+
"dash_para_warn": 2, # 单段 —— 数 ≥2:报警
|
|
121
|
+
"bold_per_1000": 8, # 加粗密度上限
|
|
122
|
+
"tics_per_1000": 8, # AI 腔词密度上限
|
|
123
|
+
"numbers_per_1000": 6, # 数字密度下限(低于=空话多)
|
|
124
|
+
"visuals_per_1000": 1.2, # 表格/图/清单密度下限(低于=枯燥)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
SENT_SPLIT = re.compile(r"[。!?;\n]+")
|
|
128
|
+
CN_CHAR = re.compile(r"[\u4e00-\u9fff]")
|
|
129
|
+
NUM = re.compile(r"\d+(?:\.\d+)?%?|\d+\.\d+|"
|
|
130
|
+
r"[一二三四五六七八九十百千万亿]+(?:个|家|元|万|亿|%|年|月|倍|人|页|天)")
|
|
131
|
+
VISUAL_LINE = re.compile(r"^\s*(\||\s*[-*+]\s|\d+\.\s|>\s|```|!\[)")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def strip_code(text: str) -> str:
|
|
135
|
+
"""去掉围栏代码与行内代码:规则文档里的反例通常放在这两种容器里,不该被判违规。"""
|
|
136
|
+
text = re.sub(r"```.*?```", "", text, flags=re.S)
|
|
137
|
+
return re.sub(r"`[^`\n]+`", "", text)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def struck_out(text: str, start: int, end: int) -> bool:
|
|
141
|
+
"""上下文白名单:命中即不计数。"""
|
|
142
|
+
ctx = text[max(0, start - 8): end + 8]
|
|
143
|
+
return any(re.search(p, ctx) for p in FALSE_POSITIVE_GUARD)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parallelism_allowed(n: int) -> int:
|
|
147
|
+
"""对偶句豁免额度(含术语定义 1 处 + 论证骨架 1 处)。"""
|
|
148
|
+
if n < 300:
|
|
149
|
+
return 1
|
|
150
|
+
if n < 1000:
|
|
151
|
+
return 2
|
|
152
|
+
return max(2, n // 300)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def is_quoted_pattern(text: str, start: int, end: int) -> bool:
|
|
156
|
+
"""命中处在「引用某个句式」而不是「使用某个句式」。
|
|
157
|
+
|
|
158
|
+
例子:标题里写「(真正的…不是…而是…)」是在给句式命名,
|
|
159
|
+
正文里写「真正的瓶颈不是产能,而是渠道库存」才是使用。
|
|
160
|
+
判据:命中片段含省略号、紧挨中文引号/括号、或落在 Markdown 引用块里。
|
|
161
|
+
"""
|
|
162
|
+
frag = text[start:end]
|
|
163
|
+
if "…" in frag or "..." in frag:
|
|
164
|
+
return True
|
|
165
|
+
before = text[max(0, start - 2):start]
|
|
166
|
+
after = text[end:end + 2]
|
|
167
|
+
openers = "「『【((“‘《〈"
|
|
168
|
+
closers = "」』】))”’》〉"
|
|
169
|
+
if any(c in openers for c in before) or any(c in closers for c in after):
|
|
170
|
+
return True
|
|
171
|
+
line_start = text.rfind("\n", 0, start) + 1
|
|
172
|
+
if text[line_start:line_start + 2].lstrip().startswith(">"):
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def find_dual_spans(text: str):
|
|
178
|
+
"""找出所有对偶转折句,并按字符区间合并重叠命中。
|
|
179
|
+
|
|
180
|
+
多条正则会命中同一句话(例如「真正的瓶颈不是产能而是渠道」会同时被
|
|
181
|
+
「不是…而是…」和「真正的…不是…是…」匹配),不合并就会把一句话算成三处,
|
|
182
|
+
把合法的单次对比误判成超标。合并后一句话只算一处。
|
|
183
|
+
"""
|
|
184
|
+
spans = []
|
|
185
|
+
for pat in JUDGE_BANNED["对偶转折句"]:
|
|
186
|
+
for m in re.finditer(pat, text):
|
|
187
|
+
if struck_out(text, m.start(), m.end()):
|
|
188
|
+
continue
|
|
189
|
+
if is_quoted_pattern(text, m.start(), m.end()):
|
|
190
|
+
continue
|
|
191
|
+
spans.append((m.start(), m.end(), m.group(0)[:26]))
|
|
192
|
+
spans.sort(key=lambda x: (x[0], -x[1]))
|
|
193
|
+
merged = []
|
|
194
|
+
for s, e, t in spans:
|
|
195
|
+
if merged and s < merged[-1][1]: # 有重叠 → 合并
|
|
196
|
+
ps, pe, pt = merged[-1]
|
|
197
|
+
merged[-1] = (ps, max(pe, e), pt if len(pt) >= len(t) else t)
|
|
198
|
+
else:
|
|
199
|
+
merged.append((s, e, t))
|
|
200
|
+
return merged
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main() -> int:
|
|
204
|
+
try: # Windows 控制台默认 GBK,中文会乱码
|
|
205
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
206
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
ap = argparse.ArgumentParser()
|
|
211
|
+
ap.add_argument("path")
|
|
212
|
+
ap.add_argument("--json", action="store_true")
|
|
213
|
+
ap.add_argument("--quiet", action="store_true")
|
|
214
|
+
ap.add_argument("--scale", type=float, default=1.0,
|
|
215
|
+
help="密度阈值缩放,节级检查建议 0.6(只影响密度类阈值,不影响禁句/句长/段长)")
|
|
216
|
+
ap.add_argument("--strict", action="store_true",
|
|
217
|
+
help="严格模式:数字密度、图表密度、AI 腔密度、加粗密度不达标也判死。"
|
|
218
|
+
"写报告的语言闸用它;改别人的稿子、做体检时不用。")
|
|
219
|
+
ap.add_argument("--doc", action="store_true",
|
|
220
|
+
help="文档模式:跳过 AI 腔/加粗密度。用于检查技能文档自身——"
|
|
221
|
+
"规则表必须引用禁词,密度指标对它无意义。")
|
|
222
|
+
args = ap.parse_args()
|
|
223
|
+
|
|
224
|
+
raw = open(args.path, encoding="utf-8").read()
|
|
225
|
+
text = strip_code(raw)
|
|
226
|
+
n = len(CN_CHAR.findall(text)) or 1
|
|
227
|
+
per1000 = 1000.0 / n
|
|
228
|
+
|
|
229
|
+
paras = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
|
|
230
|
+
body = [p for p in paras if not VISUAL_LINE.match(p) and not p.startswith("#")]
|
|
231
|
+
body = body or paras
|
|
232
|
+
para_lens = [len(CN_CHAR.findall(p)) for p in body]
|
|
233
|
+
|
|
234
|
+
sentences = [s for s in SENT_SPLIT.split(text) if CN_CHAR.search(s)]
|
|
235
|
+
sent_lens = [len(CN_CHAR.findall(s)) for s in sentences]
|
|
236
|
+
|
|
237
|
+
fails, warns, metrics = [], [], {}
|
|
238
|
+
|
|
239
|
+
# ---- 判死:点名禁句
|
|
240
|
+
ban_detail = {}
|
|
241
|
+
for label, pats in JUDGE_BANNED.items():
|
|
242
|
+
if label == "对偶转折句":
|
|
243
|
+
continue # 单独按区间合并计数,见 dual_spans()
|
|
244
|
+
hits = []
|
|
245
|
+
for pat in pats:
|
|
246
|
+
for m in re.finditer(pat, text):
|
|
247
|
+
if struck_out(text, m.start(), m.end()):
|
|
248
|
+
continue
|
|
249
|
+
hits.append(m.group(0)[:26])
|
|
250
|
+
ban_detail[label] = hits
|
|
251
|
+
|
|
252
|
+
dual_spans = find_dual_spans(text)
|
|
253
|
+
ban_detail["对偶转折句"] = [t for _, _, t in dual_spans]
|
|
254
|
+
ban_total = sum(len(v) for v in ban_detail.values())
|
|
255
|
+
metrics["禁句总数"] = ban_total
|
|
256
|
+
metrics["对偶句豁免额度"] = parallelism_allowed(n)
|
|
257
|
+
|
|
258
|
+
dual = len(dual_spans)
|
|
259
|
+
if dual > metrics["对偶句豁免额度"]:
|
|
260
|
+
fails.append({
|
|
261
|
+
"rule": "对偶转折句超限",
|
|
262
|
+
"value": f"{dual} 处(额度 {metrics['对偶句豁免额度']})",
|
|
263
|
+
"samples": ban_detail["对偶转折句"][:3],
|
|
264
|
+
"fix": "删掉否定半句,只留成立的那个判断;术语定义与论证骨架各留 1 处额度。",
|
|
265
|
+
})
|
|
266
|
+
if ban_detail["自我更正元叙述"]:
|
|
267
|
+
fails.append({
|
|
268
|
+
"rule": "报告正文出现自我更正",
|
|
269
|
+
"value": f"{len(ban_detail['自我更正元叙述'])} 处",
|
|
270
|
+
"samples": ban_detail["自我更正元叙述"][:3],
|
|
271
|
+
"fix": "直接给正确版本,不叙述修正过程;确需记录就单开「修订记录」小节。",
|
|
272
|
+
})
|
|
273
|
+
if ban_detail["生造黑话"]:
|
|
274
|
+
fails.append({
|
|
275
|
+
"rule": "生造黑话",
|
|
276
|
+
"value": f"{len(ban_detail['生造黑话'])} 处",
|
|
277
|
+
"samples": ban_detail["生造黑话"][:4],
|
|
278
|
+
"fix": "换成人话:钉死→确定下来,拉齐→统一口径,抓手→着手点,小疤→小问题。",
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
# ---- 工程词外泄(--doc 模式跳过:规则文档本来就要讨论这些词)
|
|
282
|
+
if not args.doc:
|
|
283
|
+
leaks = [m.group(0) for p in TOOL_LEAK_JUDGE for m in re.finditer(p, text)]
|
|
284
|
+
if leaks:
|
|
285
|
+
fails.append({
|
|
286
|
+
"rule": "工程词外泄",
|
|
287
|
+
"value": f"{len(leaks)} 处",
|
|
288
|
+
"samples": leaks[:4],
|
|
289
|
+
"fix": "换成读者词:报警项→需要留意的地方,判死→不合格,落盘→存成文件,"
|
|
290
|
+
"跑通→验证通过。术语只在讨论工具本身时出现。",
|
|
291
|
+
})
|
|
292
|
+
soft = [m.group(0) for p in TOOL_LEAK_WARN for m in re.finditer(p, text)]
|
|
293
|
+
if soft:
|
|
294
|
+
warns.append({"rule": "工程味偏重", "value": f"{len(soft)} 处",
|
|
295
|
+
"samples": soft[:4],
|
|
296
|
+
"fix": "对外报告可换成「检查项 / 关卡 / 一轮」这类日常说法。"})
|
|
297
|
+
meta = [m.group(0) for p in METAPHOR_WARN for m in re.finditer(p, text)]
|
|
298
|
+
if meta:
|
|
299
|
+
warns.append({"rule": "出现比喻或借代", "value": f"{len(meta)} 处",
|
|
300
|
+
"samples": meta[:4],
|
|
301
|
+
"fix": "直接说做了什么:骨架→结构,台账→清单,抓手→着手点,"
|
|
302
|
+
"打穿→彻底解决。对外报告里不用比喻。"})
|
|
303
|
+
|
|
304
|
+
# ---- 判死:标题层级(一级标题下不得直接出现三级;不鼓励四级及以下)
|
|
305
|
+
heads = [(len(m.group(1)), m.group(2).strip())
|
|
306
|
+
for m in re.finditer(r"^(#{1,6})\s+(.+)$", raw, flags=re.M)]
|
|
307
|
+
if heads:
|
|
308
|
+
deepest = max(lv for lv, _ in heads)
|
|
309
|
+
prev_level = 0
|
|
310
|
+
jumped = False
|
|
311
|
+
for lv, _ in heads:
|
|
312
|
+
if lv > prev_level + 1 and lv >= 3:
|
|
313
|
+
jumped = True
|
|
314
|
+
prev_level = lv
|
|
315
|
+
if jumped:
|
|
316
|
+
fails.append({
|
|
317
|
+
"rule": "标题层级跳级",
|
|
318
|
+
"value": "一级标题下直接出现三级(或更深)",
|
|
319
|
+
"fix": "补一级下的二级标题,或把该节降为二级。",
|
|
320
|
+
})
|
|
321
|
+
if deepest >= 4:
|
|
322
|
+
warns.append({"rule": "出现四级及以下标题",
|
|
323
|
+
"value": f"最深 {deepest} 级",
|
|
324
|
+
"fix": "并列内容改用项目列表,少用四级标题。"})
|
|
325
|
+
|
|
326
|
+
# ---- 判死:句长 / 段长 / 破折号
|
|
327
|
+
long_sents = [x for x in sent_lens if x > THRESHOLDS["sentence_hard"]]
|
|
328
|
+
if long_sents:
|
|
329
|
+
fails.append({
|
|
330
|
+
"rule": "超长句",
|
|
331
|
+
"value": f"{len(long_sents)} 句 > {THRESHOLDS['sentence_hard']} 字(最长 {max(long_sents)})",
|
|
332
|
+
"fix": "拆句:一句只装一个意思,超过 40 字就找句号的位置。",
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
if para_lens and max(para_lens) > THRESHOLDS["para_hard"]:
|
|
336
|
+
fails.append({
|
|
337
|
+
"rule": "超长段",
|
|
338
|
+
"value": f"最长 {max(para_lens)} 字 > {THRESHOLDS['para_hard']}",
|
|
339
|
+
"fix": "拆段或删铺垫:一段一个主张,段首句就是主张。",
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
# 「——」由两个「—」组成,直接相加会把一处破折号算成三处
|
|
343
|
+
def dash_count(p: str) -> int:
|
|
344
|
+
pairs = p.count("——")
|
|
345
|
+
return pairs + max(0, p.count("—") - 2 * pairs)
|
|
346
|
+
|
|
347
|
+
dash_by_para = [dash_count(p) for p in body]
|
|
348
|
+
if dash_by_para and max(dash_by_para) >= THRESHOLDS["dash_para_hard"]:
|
|
349
|
+
fails.append({
|
|
350
|
+
"rule": "破折号滥用",
|
|
351
|
+
"value": f"单段最多 {max(dash_by_para)} 处",
|
|
352
|
+
"fix": "破折号换句号/冒号/括号,或直接断句。",
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
# ---- 报警:AI 腔词
|
|
356
|
+
tic_total = 0
|
|
357
|
+
for label, pats in WARN_TICS.items():
|
|
358
|
+
hits = []
|
|
359
|
+
for pat in pats:
|
|
360
|
+
for m in re.finditer(pat, text):
|
|
361
|
+
hits.append(m.group(0)[:18])
|
|
362
|
+
tic_total += len(hits)
|
|
363
|
+
metrics[f"腔:{label}"] = len(hits)
|
|
364
|
+
if hits:
|
|
365
|
+
warns.append({
|
|
366
|
+
"rule": f"AI腔/{label}",
|
|
367
|
+
"value": f"{len(hits)} 处",
|
|
368
|
+
"samples": hits[:4],
|
|
369
|
+
"fix": "删连接词直接接事实,或把形容词换成数字。",
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
# ---- 报警:句长/段长/破折号轻档
|
|
373
|
+
mid_sents = [x for x in sent_lens if THRESHOLDS["sentence_warn"] < x <= THRESHOLDS["sentence_hard"]]
|
|
374
|
+
if mid_sents:
|
|
375
|
+
warns.append({"rule": "偏长句", "value": f"{len(mid_sents)} 句 40–60 字",
|
|
376
|
+
"fix": "拆成两句,或删掉句中的解释性从句。"})
|
|
377
|
+
if para_lens and max(para_lens) > THRESHOLDS["para_warn"] >= 0:
|
|
378
|
+
over = [x for x in para_lens if x > THRESHOLDS["para_warn"]]
|
|
379
|
+
if over and max(para_lens) <= THRESHOLDS["para_hard"]:
|
|
380
|
+
warns.append({"rule": "偏长段", "value": f"{len(over)} 段 200–300 字",
|
|
381
|
+
"fix": "拆段,手机一屏读不完的段落读者会跳过。"})
|
|
382
|
+
if dash_by_para and THRESHOLDS["dash_para_warn"] <= max(dash_by_para) < THRESHOLDS["dash_para_hard"]:
|
|
383
|
+
warns.append({"rule": "破折号偏多", "value": f"单段最多 {max(dash_by_para)} 处",
|
|
384
|
+
"fix": "一段最多保留一处真正承担插入的破折号。"})
|
|
385
|
+
|
|
386
|
+
nom = sum(len(re.findall(p, text)) for p in NOMINALIZATION)
|
|
387
|
+
metrics["名词化"] = nom
|
|
388
|
+
|
|
389
|
+
# ---- 密度
|
|
390
|
+
metrics["中文字数"] = n
|
|
391
|
+
metrics["段落数"] = len(body)
|
|
392
|
+
metrics["平均段长"] = round(sum(para_lens) / max(len(para_lens), 1), 1)
|
|
393
|
+
metrics["最长段"] = max(para_lens) if para_lens else 0
|
|
394
|
+
metrics["平均句长"] = round(sum(sent_lens) / max(len(sent_lens), 1), 1)
|
|
395
|
+
metrics["最长句"] = max(sent_lens) if sent_lens else 0
|
|
396
|
+
metrics["破折号峰值/段"] = max(dash_by_para) if dash_by_para else 0
|
|
397
|
+
metrics["加粗/千字"] = round(len(re.findall(r"\*\*[^*]+\*\*", text)) * per1000, 2)
|
|
398
|
+
metrics["AI腔/千字"] = round(tic_total * per1000, 2)
|
|
399
|
+
metrics["数字/千字"] = round(len(NUM.findall(text)) * per1000, 2)
|
|
400
|
+
visuals = sum(1 for line in raw.splitlines() if VISUAL_LINE.match(line))
|
|
401
|
+
metrics["图表清单数"] = visuals
|
|
402
|
+
metrics["图表/千字"] = round(visuals * per1000, 2)
|
|
403
|
+
|
|
404
|
+
def warn_density(key, limit, lower_bad, label, fix, min_chars=0, mode="upper"):
|
|
405
|
+
"""密度类阈值默认只报警;--strict 时升级为判死。
|
|
406
|
+
|
|
407
|
+
--scale 的方向:上限型(越少越好,如 AI 腔、加粗)用 limit/scale,
|
|
408
|
+
下限型(越多越好,如数字、图表)用 limit*scale。这样 --scale 0.6
|
|
409
|
+
对短节是「放宽」而不是「收紧」——短文本的密度天然抖动大。
|
|
410
|
+
"""
|
|
411
|
+
if n < min_chars:
|
|
412
|
+
return
|
|
413
|
+
v = metrics[key]
|
|
414
|
+
lim = limit / s if mode == "upper" else limit * s
|
|
415
|
+
bad = v < lim if lower_bad else v > lim
|
|
416
|
+
if not bad:
|
|
417
|
+
return
|
|
418
|
+
item = {"rule": label, "value": v, "limit": round(lim, 2), "fix": fix}
|
|
419
|
+
(fails if args.strict else warns).append(item)
|
|
420
|
+
|
|
421
|
+
s = args.scale
|
|
422
|
+
if not args.doc: # 规则文档要列举禁词与反例,密度类指标对它没有意义
|
|
423
|
+
warn_density("AI腔/千字", THRESHOLDS["tics_per_1000"], False,
|
|
424
|
+
"AI 腔词密度超标", "按上面每类的 fix 逐条改。", mode="upper")
|
|
425
|
+
warn_density("加粗/千字", THRESHOLDS["bold_per_1000"], False,
|
|
426
|
+
"加粗过多", "只加粗结论和数字,一段最多一处。", mode="upper")
|
|
427
|
+
warn_density("数字/千字", THRESHOLDS["numbers_per_1000"], True,
|
|
428
|
+
"数字密度过低(空话多)", "每个论断补数字/时间/金额/人名;补不出的论断删掉。",
|
|
429
|
+
mode="lower")
|
|
430
|
+
# 短节不判图表密度:200 字的节不该被要求塞一张表
|
|
431
|
+
warn_density("图表/千字", THRESHOLDS["visuals_per_1000"], True,
|
|
432
|
+
"表格清单过少(枯燥)", "并列 ≥3 项一律表格化;每 800 字至少一处表格或清单。",
|
|
433
|
+
min_chars=400, mode="lower")
|
|
434
|
+
|
|
435
|
+
passed = not fails
|
|
436
|
+
out = {"file": args.path, "passed": passed, "metrics": metrics,
|
|
437
|
+
"fails": fails, "warns": warns}
|
|
438
|
+
|
|
439
|
+
if args.json:
|
|
440
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
441
|
+
elif args.quiet:
|
|
442
|
+
print("PASS" if passed else "FAIL")
|
|
443
|
+
for f in fails:
|
|
444
|
+
print(f" ✗ {f['rule']}: {f['value']} 样例 {f.get('samples', '')}")
|
|
445
|
+
print(f" 改法:{f['fix']}")
|
|
446
|
+
for w in warns:
|
|
447
|
+
print(f" ⚠ {w['rule']}: {w['value']}")
|
|
448
|
+
else:
|
|
449
|
+
print(f"== {args.path} ==")
|
|
450
|
+
print(f"字数 {n} / 段 {len(body)} / 图表清单 {visuals} | "
|
|
451
|
+
f"平均段长 {metrics['平均段长']} 最长段 {metrics['最长段']} | "
|
|
452
|
+
f"平均句长 {metrics['平均句长']} 最长句 {metrics['最长句']}")
|
|
453
|
+
print(f"数字 {metrics['数字/千字']}/千字 · AI腔 {metrics['AI腔/千字']}/千字 · "
|
|
454
|
+
f"禁句 {ban_total} 处(对偶额度 {metrics['对偶句豁免额度']})")
|
|
455
|
+
for f in fails:
|
|
456
|
+
print(f" ✗ [判死] {f['rule']}: {f['value']} 样例 {f.get('samples', '')}")
|
|
457
|
+
print(f" 改法:{f['fix']}")
|
|
458
|
+
for w in warns:
|
|
459
|
+
print(f" ⚠ [报警] {w['rule']}: {w['value']} → {w['fix']}")
|
|
460
|
+
print("结论:", "PASS" if passed else "FAIL")
|
|
461
|
+
|
|
462
|
+
return 0 if passed else 1
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
if __name__ == "__main__":
|
|
466
|
+
sys.exit(main())
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# 评估 rubric 模板(复制到工作区,填权重与达标线)
|
|
2
|
+
|
|
3
|
+
评估对象:`REPORT.md`(或本轮交付物)。评估者:独立子代理,**没有看过产出者的推理过程**。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 一、评估者的输入(只给这三样)
|
|
8
|
+
|
|
9
|
+
1. 被评文档全文。
|
|
10
|
+
2. 本 rubric(填好的权重与达标线)。
|
|
11
|
+
3. 用户原始诉求(要什么、给谁看、抱怨了什么)。
|
|
12
|
+
|
|
13
|
+
不给:产出者的自评、产出者的改进计划、「我哪里没写好」。给了就等于让产出者自己判卷。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 二、评分维度(默认权重,可调)
|
|
18
|
+
|
|
19
|
+
| 维度 | 权重 | 机器判 / 人判 | 检查点 |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| **D1 来源与事实** | 25 | 机器判 + 抽查 | 每条硬结论有 S 编号;百分比带分母与来源;来源分级正确;转载去重;陈旧数据标注;承重引用开页核对过 |
|
|
22
|
+
| **D2 洞察与机制** | 25 | 人判 | 除「是什么」外给了「为什么」(竞争假说,不是单一解释);有反证段且诚实(列替代解释→怎么排除→没排除掉的→结论降级);冲突有解释与下一步 |
|
|
23
|
+
| **D3 语言与可读性** | 20 | 机器判 + 人判 | `report_lint.py --strict` PASS;对偶句不超豁免额度;无自我更正、无生造黑话;句长段长在阈值内;逐句自问「删掉这句,读者判断会变吗」,答「不变」的句子已删 |
|
|
24
|
+
| **D4 结构** | 15 | 机器判 | 首段给结论;六章齐全(或按档位合并后齐全);标题是判断句;一节一个主张;局限单独成节 |
|
|
25
|
+
| **D5 篇幅与呈现** | 15 | 机器判 | 字数在 `SKILL.md` 第 0 步的档位区间;图表数量达标且标题写结论;附录 ≤10%;对比总表可独立读懂 |
|
|
26
|
+
|
|
27
|
+
**打分口径**:每个维度按 0–100 打分(该维度的完成度),总分 = Σ(维度分 × 权重) ÷ 100。权重合计 100。达标线默认 **85**,且任一硬伤即不达标(总分无效)。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 三、硬伤(命中任一 → 本轮不达标)
|
|
32
|
+
|
|
33
|
+
- **H1 编造**:出现证据表里没有的数字、机构、日期、引文。
|
|
34
|
+
- **H2 无源硬结论**:决策级结论没有任何 S 编号,或仅由 C 级来源支撑。
|
|
35
|
+
- **H3 缺反证**:没有「没排除掉的替代解释」这一段。
|
|
36
|
+
- **H4 机器闸失败**:`report_lint.py` 返回 FAIL 且未在报告里记录豁免理由。
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 四、差距清单(评估的唯一有效输出)
|
|
41
|
+
|
|
42
|
+
每条五列,缺一列视为不合格评估,退回重评:
|
|
43
|
+
|
|
44
|
+
| # | 位置 | 现状 | 目标 | 改法 | 严重度 |
|
|
45
|
+
|---|---|---|---|---|---|
|
|
46
|
+
| 1 | 第 3 节「欧洲退坡」 | 只有结论,没有回本期数据 | 列出回本期与电价两条硬数据 | 从 S02/S05 取数补一句 | 高 |
|
|
47
|
+
| 2 | 第 5 节第 2 段 | 段落 420 字,读不下去 | ≤300 字 | 拆成两段,删掉铺垫句 | 中 |
|
|
48
|
+
|
|
49
|
+
规矩:
|
|
50
|
+
|
|
51
|
+
- 只能写「达标」或「具体差距」,不许写「还行」「可以更丰富」「建议多加点数据」。
|
|
52
|
+
- 位置要能定位到节 / 段 / 句。
|
|
53
|
+
- 严重度只有三档:高(影响结论)、中(影响阅读)、低(措辞)。
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 五、打分方法
|
|
58
|
+
|
|
59
|
+
1. 先跑机器项:`python report_lint.py REPORT.md` + 字数统计 + 来源计数,把数字记下来。
|
|
60
|
+
2. 再逐维度给分,每个维度必须引 1–3 段原文作为依据。
|
|
61
|
+
3. 总分 = Σ(维度分 × 权重) / 100。
|
|
62
|
+
4. 输出顺序固定:总分 → 是否达标 → 硬伤 → 各维度分与依据 → 差距清单。
|
|
63
|
+
|
|
64
|
+
**校准锚点**
|
|
65
|
+
|
|
66
|
+
| 分数 | 含义 |
|
|
67
|
+
|---|---|
|
|
68
|
+
| 95+ | 可直接对外发,规则细到不用再解释 |
|
|
69
|
+
| 85–94 | 达标,有 1–2 处可优化 |
|
|
70
|
+
| 70–84 | 方向对,某一维度明显薄弱 |
|
|
71
|
+
| <70 | 需要重写而不是修补 |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 六、轮次与退出
|
|
76
|
+
|
|
77
|
+
- 上限 3 轮(用户指定则从用户)。
|
|
78
|
+
- 每轮记录:轮次 / 总分 / 各维度 / 差距条数 / 本轮改了什么。
|
|
79
|
+
- 达标 → 交付。
|
|
80
|
+
- 到上限未达标 → 交付当前最好版本 + 剩余差距,明说未达标。
|
|
81
|
+
- 停滞(连续两轮同一维度不涨)→ 早停,说明「该维度靠改文字解决不了,需补证据或换角度」。
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 七、防止自己糊弄自己
|
|
86
|
+
|
|
87
|
+
- 评估者与产出者必须是不同 agent。
|
|
88
|
+
- 同一轮里不许中途放低达标线。要改阈值,写在下一轮的记录里并说明理由。
|
|
89
|
+
- 「全部复制原文也能过」是失败信号:评估必须能指出哪几句是新增的解释层。
|