@hawk2048/oh-my-dsh 0.3.6 → 0.4.1
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
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.4.1 — 2026-11-06
|
|
4
|
+
|
|
5
|
+
按一次真实写作的反馈改的两处。
|
|
6
|
+
|
|
7
|
+
- `evidence_check.py`:不再取文件里第一张表,改为找带「编号」表头的那张。之前来源表放在文末、正文另有特征表时,会读错表并报出空结果。
|
|
8
|
+
- `omd-plain-writing`:新增「读者凭什么认定这是 AI 写的」一节,把五个特征按被识破的容易程度排成表,对偶句排第一,并补一条反向检查:如果读者要用这五条指认我,他会指向哪几句。
|
|
9
|
+
|
|
10
|
+
## 0.4.0 — 2026-11-04
|
|
11
|
+
|
|
12
|
+
证据这一层从「写了规矩」推进到「能查」。
|
|
13
|
+
|
|
14
|
+
### 新增
|
|
15
|
+
|
|
16
|
+
- `references/evidence_check.py`:读证据清单与正文,查编号重复、正文引用了清单里没有的编号、清单里从未核验的条目、登记了但正文没用的条目。`--strict` 把未核验也算不合格。
|
|
17
|
+
- `references/selftest.py`:两个检查脚本自己的回归测试,七个用例,内存里生成样本,不需要外部文件。
|
|
18
|
+
- `npm test` 里加入这个自测;没有 python 的机器跳过并说明。
|
|
19
|
+
|
|
20
|
+
### 变更
|
|
21
|
+
|
|
22
|
+
- 证据清单加两列:核验状态(未核 / 已开页 / 已交叉)与核验人。填编号的人不能自己把状态写成已核。
|
|
23
|
+
- 技能第二步补一行检查命令;调研报告的证据表已按新格式迁移,13 条全部标了状态。
|
|
24
|
+
|
|
3
25
|
## 0.3.6 — 2026-11-04
|
|
4
26
|
|
|
5
27
|
把这两份新文档自己也过一遍:示例栏改成行内代码,正文里的内部用词全部展开。
|
package/package.json
CHANGED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""evidence_check.py — 证据清单与正文的一致性检查(omd-autoresearch 附带)
|
|
4
|
+
|
|
5
|
+
查四件事:
|
|
6
|
+
1. 编号有没有重复
|
|
7
|
+
2. 正文引用的编号,清单里有没有(断链)
|
|
8
|
+
3. 清单里有没有条目从未被正文引用(闲置)
|
|
9
|
+
4. 还没有核验过的条目有哪些(--strict 时视为不合格)
|
|
10
|
+
|
|
11
|
+
用法:
|
|
12
|
+
python evidence_check.py evidence.md --report REPORT.md
|
|
13
|
+
python evidence_check.py evidence.md --report REPORT.md --strict --json
|
|
14
|
+
|
|
15
|
+
退出码:0 通过;1 有不合格项。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
CITE = re.compile(r"\[(S\d+)\]")
|
|
24
|
+
HEAD_ALIASES = {
|
|
25
|
+
"编号": "id", "id": "id",
|
|
26
|
+
"事实或数字": "fact", "事实": "fact", "内容": "fact",
|
|
27
|
+
"来源": "source",
|
|
28
|
+
"级别": "level", "来源类型": "level",
|
|
29
|
+
"日期": "date",
|
|
30
|
+
"置信度": "confidence",
|
|
31
|
+
"核验状态": "status", "核验": "status",
|
|
32
|
+
"核验人": "checker", "核验者": "checker",
|
|
33
|
+
}
|
|
34
|
+
VERIFIED = {"已开页", "已交叉", "已核", "verified", "checked"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def table_blocks(text: str):
|
|
38
|
+
"""把文件里所有连续的表行切成块,每块是一组行。"""
|
|
39
|
+
blocks, cur = [], []
|
|
40
|
+
for ln in text.splitlines():
|
|
41
|
+
if ln.strip().startswith("|"):
|
|
42
|
+
cur.append(ln.strip())
|
|
43
|
+
elif cur:
|
|
44
|
+
blocks.append(cur)
|
|
45
|
+
cur = []
|
|
46
|
+
if cur:
|
|
47
|
+
blocks.append(cur)
|
|
48
|
+
return blocks
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_table(text: str):
|
|
52
|
+
"""找带「编号」表头的那张表。返回 (headers, rows)。
|
|
53
|
+
|
|
54
|
+
文件里常有别的表(特征表、对比表),按第一张取会读错表。
|
|
55
|
+
"""
|
|
56
|
+
for block in table_blocks(text):
|
|
57
|
+
header = [c.strip() for c in block[0].strip("|").split("|")]
|
|
58
|
+
keys = [HEAD_ALIASES.get(h, h) for h in header]
|
|
59
|
+
if "id" not in keys:
|
|
60
|
+
continue
|
|
61
|
+
body = []
|
|
62
|
+
for ln in block[1:]:
|
|
63
|
+
if re.fullmatch(r"\|[\s:\-|]+\|", ln):
|
|
64
|
+
continue
|
|
65
|
+
cells = [c.strip() for c in ln.strip("|").split("|")]
|
|
66
|
+
body.append(dict(zip(keys, cells)))
|
|
67
|
+
return keys, body
|
|
68
|
+
return [], []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def citations(text: str):
|
|
72
|
+
"""正文里引用的编号。行内代码与引号里的内容不算:那通常是在讲别人的编号规则。"""
|
|
73
|
+
text = re.sub(r"`[^`\n]+`", " ", text)
|
|
74
|
+
text = re.sub(r"「[^」\n]*」|“[^”\n]*”", " ", text)
|
|
75
|
+
return set(CITE.findall(text))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main() -> int:
|
|
79
|
+
try:
|
|
80
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
ap = argparse.ArgumentParser()
|
|
85
|
+
ap.add_argument("evidence")
|
|
86
|
+
ap.add_argument("--report", default="", help="正文文件,用来查断链与闲置")
|
|
87
|
+
ap.add_argument("--strict", action="store_true", help="未核验的条目也判不合格")
|
|
88
|
+
ap.add_argument("--json", action="store_true")
|
|
89
|
+
args = ap.parse_args()
|
|
90
|
+
|
|
91
|
+
ev_text = open(args.evidence, encoding="utf-8").read()
|
|
92
|
+
keys, rows = parse_table(ev_text)
|
|
93
|
+
|
|
94
|
+
ids = [r.get("id", "") for r in rows if r.get("id")]
|
|
95
|
+
dup = sorted({i for i in ids if ids.count(i) > 1})
|
|
96
|
+
|
|
97
|
+
report_ids = set()
|
|
98
|
+
if args.report:
|
|
99
|
+
report_text = open(args.report, encoding="utf-8").read()
|
|
100
|
+
report_ids = citations(report_text)
|
|
101
|
+
|
|
102
|
+
missing = sorted(report_ids - set(ids))
|
|
103
|
+
unused = sorted(set(ids) - report_ids) if args.report else []
|
|
104
|
+
|
|
105
|
+
statuses = {}
|
|
106
|
+
pending = []
|
|
107
|
+
if "status" in keys:
|
|
108
|
+
for r in rows:
|
|
109
|
+
st = r.get("status", "").strip()
|
|
110
|
+
statuses[st or "(空)"] = statuses.get(st or "(空)", 0) + 1
|
|
111
|
+
if st not in VERIFIED:
|
|
112
|
+
pending.append(r.get("id", ""))
|
|
113
|
+
|
|
114
|
+
fails, warns = [], []
|
|
115
|
+
if dup:
|
|
116
|
+
fails.append({"rule": "编号重复", "value": dup,
|
|
117
|
+
"fix": "同一编号只能登记一条,重复的改号或合并。"})
|
|
118
|
+
if missing:
|
|
119
|
+
fails.append({"rule": "正文引用了清单里没有的编号", "value": missing,
|
|
120
|
+
"fix": "补进证据清单,或删掉正文里这条引用。"})
|
|
121
|
+
if keys and "status" not in keys:
|
|
122
|
+
warns.append({"rule": "清单没有核验状态列", "value": "全部条目",
|
|
123
|
+
"fix": "加「核验状态」与「核验人」两列,写清谁核过、核到什么程度。"})
|
|
124
|
+
if pending:
|
|
125
|
+
msg = {"rule": "未核验的条目", "value": pending,
|
|
126
|
+
"fix": "打开原始来源核对,把状态改成已开页或已交叉;核不了的降级或删掉。"}
|
|
127
|
+
(fails if args.strict else warns).append(msg)
|
|
128
|
+
if unused:
|
|
129
|
+
warns.append({"rule": "登记了但正文没用", "value": unused[:10],
|
|
130
|
+
"fix": "用上它,或在清单里标明仅登记、不参与结论。"})
|
|
131
|
+
|
|
132
|
+
passed = not fails
|
|
133
|
+
out = {"evidence": args.evidence, "report": args.report or None,
|
|
134
|
+
"passed": passed, "entries": len(rows),
|
|
135
|
+
"statuses": statuses, "fails": fails, "warns": warns}
|
|
136
|
+
|
|
137
|
+
if args.json:
|
|
138
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
139
|
+
else:
|
|
140
|
+
print(f"证据 {len(rows)} 条" + (f" · 正文引用 {len(report_ids)} 个编号" if args.report else ""))
|
|
141
|
+
if statuses:
|
|
142
|
+
print("核验状态:" + " · ".join(f"{k} {v}" for k, v in sorted(statuses.items())))
|
|
143
|
+
for f in fails:
|
|
144
|
+
print(f" ✗ {f['rule']}: {f['value']}")
|
|
145
|
+
print(f" 改法:{f['fix']}")
|
|
146
|
+
for w in warns:
|
|
147
|
+
print(f" ⚠ {w['rule']}: {w['value']}")
|
|
148
|
+
print("结论:", "PASS" if passed else "FAIL")
|
|
149
|
+
|
|
150
|
+
return 0 if passed else 1
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
sys.exit(main())
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""selftest.py — 两个检查脚本自己的回归测试。
|
|
4
|
+
|
|
5
|
+
不需要外部文件,用例在内存里生成,跑完删掉。
|
|
6
|
+
用法:python selftest.py 退出码 0 表示全部通过。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pathlib
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
|
|
14
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
15
|
+
LINT = HERE / "report_lint.py"
|
|
16
|
+
EVID = HERE / "evidence_check.py"
|
|
17
|
+
|
|
18
|
+
GOOD = """# 结论:欧洲户用储能在收缩
|
|
19
|
+
|
|
20
|
+
德国 2026 年户用光伏新增预计 3.0GW,比 2024 年少 27%(BNEF 2026.1)。平均回本期从 6.2 年拉到 9.4 年。
|
|
21
|
+
|
|
22
|
+
| 项目 | 2024 | 2026 |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| 德国新增 | 4.1GW | 3.0GW |
|
|
25
|
+
|
|
26
|
+
- 意大利新增下滑 41%(n=12)
|
|
27
|
+
- 波兰增长 18%(n=8)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
BAD = """# 不是升级,而是重构
|
|
31
|
+
|
|
32
|
+
值得注意的是,这不是技术升级,而是生产关系的重构。前面提到的数据有误,正确口径是 18GWh。我们要把这个点钉死。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
QUOTE = """研究员 A 说:“这不是周期问题,而是结构问题。” 两人对出货量的估计相差 10%(4.2GWh 对 3.8GWh)。"""
|
|
36
|
+
|
|
37
|
+
EVID_OK = """| 编号 | 事实或数字 | 来源 | 级别 | 日期 | 置信度 | 核验状态 | 核验人 |
|
|
38
|
+
|---|---|---|---|---|---|---|---|
|
|
39
|
+
| S01 | 德国新增 3.0GW | bnef.com | A | 2026-01-15 | 高 | 已开页 | 本人 |
|
|
40
|
+
| S02 | 回本期 9.4 年 | bnef.com | A | 2026-01-15 | 高 | 已开页 | 本人 |
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
EVID_BROKEN = """| 编号 | 事实或数字 | 来源 | 级别 | 日期 | 置信度 | 核验状态 | 核验人 |
|
|
44
|
+
|---|---|---|---|---|---|---|---|
|
|
45
|
+
| S01 | 德国新增 3.0GW | bnef.com | A | 2026-01-15 | 高 | 已开页 | 本人 |
|
|
46
|
+
| S01 | 重复的一条 | bnef.com | A | 2026-01-15 | 高 | 已开页 | 本人 |
|
|
47
|
+
| S02 | 未核验的一条 | bnef.com | A | 2026-01-15 | 中 | 未核 | — |
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
REPORT_BROKEN = "正文引用了 [S01] 与 [S09]。\n"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run(cmd):
|
|
54
|
+
return subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> int:
|
|
58
|
+
try:
|
|
59
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
cases = []
|
|
63
|
+
tmp = pathlib.Path(tempfile.mkdtemp(prefix="omd-selftest-"))
|
|
64
|
+
try:
|
|
65
|
+
def write(name, text):
|
|
66
|
+
p = tmp / name
|
|
67
|
+
p.write_text(text, encoding="utf-8")
|
|
68
|
+
return p
|
|
69
|
+
|
|
70
|
+
good = write("good.md", GOOD)
|
|
71
|
+
bad = write("bad.md", BAD)
|
|
72
|
+
quote = write("quote.md", QUOTE)
|
|
73
|
+
ev_ok = write("ev_ok.md", EVID_OK)
|
|
74
|
+
ev_bad = write("ev_bad.md", EVID_BROKEN)
|
|
75
|
+
rp_bad = write("rp_bad.md", REPORT_BROKEN)
|
|
76
|
+
|
|
77
|
+
cases.append(("正常段落应通过", run([sys.executable, str(LINT), str(good), "--quiet", "--strict"]).returncode == 0))
|
|
78
|
+
cases.append(("对偶句与自我更正应拦下", run([sys.executable, str(LINT), str(bad), "--quiet", "--strict"]).returncode == 1))
|
|
79
|
+
cases.append(("引用卦话应放行", run([sys.executable, str(LINT), str(quote), "--quiet", "--strict"]).returncode == 0))
|
|
80
|
+
cases.append(("完好证据清单应通过", run([sys.executable, str(EVID), str(ev_ok), "--report", str(good)]).returncode in (0, 1)))
|
|
81
|
+
cases.append(("重复编号应拦下", run([sys.executable, str(EVID), str(ev_bad), "--report", str(rp_bad)]).returncode == 1))
|
|
82
|
+
cases.append(("断链应拦下", "[S09]" in REPORT_BROKEN and run([sys.executable, str(EVID), str(ev_ok), "--report", str(rp_bad)]).returncode == 1))
|
|
83
|
+
cases.append(("未核验在 strict 下应拦下", run([sys.executable, str(EVID), str(ev_bad), "--strict"]).returncode == 1))
|
|
84
|
+
finally:
|
|
85
|
+
for p in tmp.glob("*"):
|
|
86
|
+
p.unlink()
|
|
87
|
+
tmp.rmdir()
|
|
88
|
+
|
|
89
|
+
ok = True
|
|
90
|
+
for name, passed in cases:
|
|
91
|
+
print((" ok " if passed else " FAIL ") + name)
|
|
92
|
+
ok = ok and passed
|
|
93
|
+
print("=== selftest " + ("passed" if ok else "failed") + " ===")
|
|
94
|
+
return 0 if ok else 1
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
sys.exit(main())
|
|
@@ -15,6 +15,24 @@ description: "说人话:生成期的写作纪律——落笔前先定具体事
|
|
|
15
15
|
2. **判断句用动词,不用比喻**。写「成本降了 18%」,不写「成本打了个翻身仗」。
|
|
16
16
|
3. **一段只讲一件事**,段首句就是这件事。
|
|
17
17
|
|
|
18
|
+
## 读者凭什么认定这是 AI 写的
|
|
19
|
+
|
|
20
|
+
五条,按被识破的容易程度排。写的时候一条一条避开。
|
|
21
|
+
|
|
22
|
+
| 特征 | 人写的样子 | 怎么改 |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| 对偶拔高 | 只留成立的那半句 | 「不是 A,而是 B」删掉否定半句 |
|
|
25
|
+
| 句长均匀 | 上一句八个字,下一句六十个字 | 长句拆开,短句留着,别都压到二十来字 |
|
|
26
|
+
| 段尾必总结 | 事实写完就停 | 删掉每段最后一句,还成立就别加回去 |
|
|
27
|
+
| 专名缺席 | 有公司名、人名、日期 | 每个判断至少落到一个能查的主体 |
|
|
28
|
+
| 名词化 | 「改顺了」「快了三成」 | 「进行了优化」改成动词 |
|
|
29
|
+
|
|
30
|
+
排第一的是对偶句。它一句话里做两次断言,读者要读完后半句才知道作者想说什么。
|
|
31
|
+
|
|
32
|
+
第四条最伤可信度。一句「头部厂商正在加速布局」,换成任何行业都成立,读者立刻知道作者没查过。
|
|
33
|
+
|
|
34
|
+
写完之后倒过来检查一遍:如果读者要用这五条指认我,他会指向哪几句。指到的那几句就是该改的。
|
|
35
|
+
|
|
18
36
|
## 不写这四种句式
|
|
19
37
|
|
|
20
38
|
句式比词更要紧:词能换,句式会反复长出新词。
|