@hawk2048/oh-my-dsh 0.3.6 → 0.4.0

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,20 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.4.0 — 2026-11-04
4
+
5
+ 证据这一层从「写了规矩」推进到「能查」。
6
+
7
+ ### 新增
8
+
9
+ - `references/evidence_check.py`:读证据清单与正文,查编号重复、正文引用了清单里没有的编号、清单里从未核验的条目、登记了但正文没用的条目。`--strict` 把未核验也算不合格。
10
+ - `references/selftest.py`:两个检查脚本自己的回归测试,七个用例,内存里生成样本,不需要外部文件。
11
+ - `npm test` 里加入这个自测;没有 python 的机器跳过并说明。
12
+
13
+ ### 变更
14
+
15
+ - 证据清单加两列:核验状态(未核 / 已开页 / 已交叉)与核验人。填编号的人不能自己把状态写成已核。
16
+ - 技能第二步补一行检查命令;调研报告的证据表已按新格式迁移,13 条全部标了状态。
17
+
3
18
  ## 0.3.6 — 2026-11-04
4
19
 
5
20
  把这两份新文档自己也过一遍:示例栏改成行内代码,正文里的内部用词全部展开。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hawk2048/oh-my-dsh",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "description": "oh-my-dsh: DeepSeek Harness multi-agent orchestration layer — OMC-style workflow skills, slash commands, and an agent preset.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -0,0 +1,147 @@
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 parse_table(text: str):
38
+ """取文件里第一张表(连续以 | 开头的行)。返回 (headers, rows)。
39
+
40
+ 只取一张:文件后面可能还有别的表(比如机制登记),混进来会串行。
41
+ """
42
+ lines = text.splitlines()
43
+ start = next((i for i, ln in enumerate(lines) if ln.strip().startswith("|")), None)
44
+ if start is None:
45
+ return [], []
46
+ block = []
47
+ for ln in lines[start:]:
48
+ if not ln.strip().startswith("|"):
49
+ break
50
+ block.append(ln.strip())
51
+ if not block:
52
+ return [], []
53
+ header = [c.strip() for c in block[0].strip("|").split("|")]
54
+ keys = [HEAD_ALIASES.get(h, h) for h in header]
55
+ body = []
56
+ for ln in block[1:]:
57
+ if re.fullmatch(r"\|[\s:\-|]+\|", ln):
58
+ continue
59
+ cells = [c.strip() for c in ln.strip("|").split("|")]
60
+ body.append(dict(zip(keys, cells)))
61
+ return keys, body
62
+
63
+
64
+ def citations(text: str):
65
+ """正文里引用的编号。行内代码与引号里的内容不算:那通常是在讲别人的编号规则。"""
66
+ text = re.sub(r"`[^`\n]+`", " ", text)
67
+ text = re.sub(r"「[^」\n]*」|“[^”\n]*”", " ", text)
68
+ return set(CITE.findall(text))
69
+
70
+
71
+ def main() -> int:
72
+ try:
73
+ sys.stdout.reconfigure(encoding="utf-8")
74
+ except Exception:
75
+ pass
76
+
77
+ ap = argparse.ArgumentParser()
78
+ ap.add_argument("evidence")
79
+ ap.add_argument("--report", default="", help="正文文件,用来查断链与闲置")
80
+ ap.add_argument("--strict", action="store_true", help="未核验的条目也判不合格")
81
+ ap.add_argument("--json", action="store_true")
82
+ args = ap.parse_args()
83
+
84
+ ev_text = open(args.evidence, encoding="utf-8").read()
85
+ keys, rows = parse_table(ev_text)
86
+
87
+ ids = [r.get("id", "") for r in rows if r.get("id")]
88
+ dup = sorted({i for i in ids if ids.count(i) > 1})
89
+
90
+ report_ids = set()
91
+ if args.report:
92
+ report_text = open(args.report, encoding="utf-8").read()
93
+ report_ids = citations(report_text)
94
+
95
+ missing = sorted(report_ids - set(ids))
96
+ unused = sorted(set(ids) - report_ids) if args.report else []
97
+
98
+ statuses = {}
99
+ pending = []
100
+ if "status" in keys:
101
+ for r in rows:
102
+ st = r.get("status", "").strip()
103
+ statuses[st or "(空)"] = statuses.get(st or "(空)", 0) + 1
104
+ if st not in VERIFIED:
105
+ pending.append(r.get("id", ""))
106
+
107
+ fails, warns = [], []
108
+ if dup:
109
+ fails.append({"rule": "编号重复", "value": dup,
110
+ "fix": "同一编号只能登记一条,重复的改号或合并。"})
111
+ if missing:
112
+ fails.append({"rule": "正文引用了清单里没有的编号", "value": missing,
113
+ "fix": "补进证据清单,或删掉正文里这条引用。"})
114
+ if keys and "status" not in keys:
115
+ warns.append({"rule": "清单没有核验状态列", "value": "全部条目",
116
+ "fix": "加「核验状态」与「核验人」两列,写清谁核过、核到什么程度。"})
117
+ if pending:
118
+ msg = {"rule": "未核验的条目", "value": pending,
119
+ "fix": "打开原始来源核对,把状态改成已开页或已交叉;核不了的降级或删掉。"}
120
+ (fails if args.strict else warns).append(msg)
121
+ if unused:
122
+ warns.append({"rule": "登记了但正文没用", "value": unused[:10],
123
+ "fix": "用上它,或在清单里标明仅登记、不参与结论。"})
124
+
125
+ passed = not fails
126
+ out = {"evidence": args.evidence, "report": args.report or None,
127
+ "passed": passed, "entries": len(rows),
128
+ "statuses": statuses, "fails": fails, "warns": warns}
129
+
130
+ if args.json:
131
+ print(json.dumps(out, ensure_ascii=False, indent=2))
132
+ else:
133
+ print(f"证据 {len(rows)} 条" + (f" · 正文引用 {len(report_ids)} 个编号" if args.report else ""))
134
+ if statuses:
135
+ print("核验状态:" + " · ".join(f"{k} {v}" for k, v in sorted(statuses.items())))
136
+ for f in fails:
137
+ print(f" ✗ {f['rule']}: {f['value']}")
138
+ print(f" 改法:{f['fix']}")
139
+ for w in warns:
140
+ print(f" ⚠ {w['rule']}: {w['value']}")
141
+ print("结论:", "PASS" if passed else "FAIL")
142
+
143
+ return 0 if passed else 1
144
+
145
+
146
+ if __name__ == "__main__":
147
+ 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())