@mingmaple/dsh-handover 0.9.22 → 1.0.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/README.md +117 -113
- package/docs/migrate.md +52 -41
- package/docs/releases/0.9.12.md +2 -3
- package/docs/releases/0.9.14.md +1 -5
- package/docs/releases/0.9.22.md +1 -1
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +221 -61
- package/package.json +5 -2
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +12 -2
- package/scripts/blank-smoke.mjs +95 -44
- package/scripts/check-docs.mjs +7 -0
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -994
- package/src/index.mjs +273 -109
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +9 -3
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +21 -6
- package/src/lib/docmap.mjs +5 -8
- package/src/lib/drift.mjs +1 -1
- package/src/lib/events.mjs +1 -1
- package/src/lib/facts.mjs +19 -12
- package/src/lib/gates.mjs +12 -12
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +80 -75
- package/src/lib/handover-io.mjs +69 -6
- package/src/lib/identity.mjs +330 -0
- package/src/lib/legacy.mjs +89 -0
- package/src/lib/lint.mjs +14 -11
- package/src/lib/manual.mjs +117 -80
- package/src/lib/mover.mjs +18 -14
- package/src/lib/office.mjs +42 -2
- package/src/lib/pack-ops.mjs +10 -10
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/phase.mjs +8 -13
- package/src/lib/profile.mjs +1 -1
- package/src/lib/reminder.mjs +110 -0
- package/src/lib/role.mjs +1 -1
- package/src/lib/scaffold.mjs +30 -10
- package/src/lib/schedule.mjs +5 -5
- package/src/lib/session-create.mjs +2 -2
- package/src/lib/session-ops.mjs +76 -47
- package/src/lib/sessions.mjs +40 -2
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -336
- package/src/lib/succession.mjs +14 -14
- package/src/lib/system.mjs +364 -0
- package/src/templates/Changelog.skeleton.md +3 -3
- package/src/templates/Charter.skeleton.md +13 -9
- package/src/templates/Handover.global.skeleton.md +20 -13
- package/src/templates/Handover.template.md +89 -0
- package/src/templates/Plan.skeleton.md +34 -0
- package/src/templates/Prompt_Templates.md +91 -10
- package/src/templates/Runtime_Manual.md +254 -139
- package/src/templates/System.skeleton.md +31 -0
- package/src/templates/Systems.global.skeleton.md +13 -0
- package/src/templates/rules/Delegation_Rules.md +45 -0
- package/src/templates/rules/Handover_Rules.md +46 -24
- package/src/templates/rules/Migration_Rules.md +7 -7
- package/src/templates/rules/Misc_Task_Standard.md +17 -10
- package/src/templates/rules/Writing_Rules.md +33 -16
- package/src/templates/standards/README.md +19 -7
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203//344/276/235/346/215/256_/350/256/272/346/226/207.md +389 -0
- package/src/tools/ho_backup.mjs +8 -8
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +34 -31
- package/src/tools/ho_docmap.mjs +135 -49
- package/src/tools/ho_gate.mjs +9 -4
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +45 -4
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +43 -22
- package/src/tools/ho_status.mjs +14 -9
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +83 -51
- package/src/tools/peer.mjs +219 -8
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 语体门禁(style-gate):按写作规范中标注「自动」的条款检查中文串。
|
|
3
|
+
*
|
|
4
|
+
* 规范正文:_Control_Center/standards/写作规范/README.md(W-01 至 W-42)。
|
|
5
|
+
* 本脚本只实现本次分配的十个条款:
|
|
6
|
+
* 阻断级(命中即退出码 1):W-01、W-02、W-04、W-05、W-07、W-08;
|
|
7
|
+
* 警告级(只命中警告时退出码 0):W-09、W-19(仅判文档)、W-26、W-38。
|
|
8
|
+
*
|
|
9
|
+
* 扫描范围:README.md、docs/**\/*.md、src/templates/**(不含 src/templates/standards/)、
|
|
10
|
+
* src/**\/*.mjs、src/client.js。代码文件只看注释与字符串字面量;文档只看正文,
|
|
11
|
+
* 不扫描 fenced code block 与行内代码段。
|
|
12
|
+
*
|
|
13
|
+
* 唯一例外 E-01:对外仓库 README.md「为什么需要」一节的指定正文豁免 W-01;按节绑定,由 check-docs 逐字比对;不得扩充。
|
|
14
|
+
*
|
|
15
|
+
* 用法:node scripts/style-gate.mjs
|
|
16
|
+
* 只读,不写任何文件;只用 node:fs 与 node:path,无第三方依赖。
|
|
17
|
+
*
|
|
18
|
+
* 输出:全部通过时一行 OK;否则逐条列出「文件:行 条款 命中内容」。
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
/* ============================== 常量表 ============================== */
|
|
24
|
+
|
|
25
|
+
/** 唯一例外 E-01:对外 README「为什么需要」一节的指定句(用户指定保留;整句不计 W-01);不得扩充。 */
|
|
26
|
+
const E01_TEXT = '本插件一定能很好地帮助你。来试试吧;不满意,随时关闭插件并删除即可。';
|
|
27
|
+
|
|
28
|
+
/** 规范正文与论文属规范性文本,条款本身会引用被禁词,整目录排除。 */
|
|
29
|
+
const SKIP_DIRS = ['src/templates/standards'];
|
|
30
|
+
|
|
31
|
+
/** W-01 语气词与口语词(规范正文原表)。 */
|
|
32
|
+
const W01_WORDS = ['吧', '呢', '啦', '嘛', '呀', '哈', '哦', '其实', '反正'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* W-01 的单字词只在作语气词时判违规:「哈希」(hash)是技术专名,不是语气词。
|
|
36
|
+
* 这只处理词形边界,不构成条款例外;其余单字仍按原表逐字判定。
|
|
37
|
+
*/
|
|
38
|
+
function isW01Word(text, at, word) {
|
|
39
|
+
return !(word === '哈' && text[at + 1] === '希');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** W-02 情绪化、评价性表达(规范正文原表)。 */
|
|
43
|
+
const W02_WORDS = ['完美', '惊艳', '糟糕', '坑', '丝滑', '离谱'];
|
|
44
|
+
|
|
45
|
+
/** W-04 填充语(规范正文原表)。 */
|
|
46
|
+
const W04_WORDS = ['值得注意的是', '众所周知', '简单来说'];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* W-05 界面动作「点」未写「点击」。
|
|
50
|
+
* 只判动词用法:后面紧跟界面对象(引号、按钮类名词、「一下」等);
|
|
51
|
+
* 节点、重点、落点、定点、清点等名词不受影响;点击、双击、单击本已合规。
|
|
52
|
+
*/
|
|
53
|
+
const W05_RE = /(?<![双单轻])点(?=一下|下(?:按钮|卡片|链接|图标|选项|菜单|标签|开关)|开|选|按|右|按钮|卡片|链接|图标|选项|菜单|标签|开关|确定|取消|刷新|「|『|“|"|'|`)/;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* W-07 表情符号(含 ✅❌⚠✗ 一类图形结论符号)与网络缩略语。
|
|
57
|
+
* 箭头、圈号等结构符号在代码注释与流程文案中作分隔标记,不在本次分配范围,
|
|
58
|
+
* 不据此判违规,以免把注释与流程标记误判为装饰。
|
|
59
|
+
*/
|
|
60
|
+
const W07_EMOJI_RE = /[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{1F1E6}-\u{1F1FF}]/u;
|
|
61
|
+
const W07_NET_ABBR = ['yyds', 'xswl', 'awsl', 'srds', 'u1s1', 'zqsg', 'dddd', 'nsdd', 'gkd', 'yygq', 'xdm'];
|
|
62
|
+
|
|
63
|
+
/** W-09 单句上限(字)。 */
|
|
64
|
+
const W09_LIMIT = 40;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* W-19 常见缩写表:缩写与其全称。只收本仓库常出现、且补全称有信息量的几个;
|
|
68
|
+
* JSON、HTTP、URL 等通用术语不在此列,避免把技术名词当违规。
|
|
69
|
+
* 缩写首次出现全称的要求只判文档(README、docs、模板),代码注释不成文,不作提示。
|
|
70
|
+
*/
|
|
71
|
+
const W19_ABBR = [
|
|
72
|
+
{ abbr: 'DSH', forms: ['DeepSeek Harness'] },
|
|
73
|
+
{ abbr: 'API', forms: ['应用程序接口', 'Application Programming Interface'] },
|
|
74
|
+
{ abbr: 'CLI', forms: ['命令行界面', 'Command Line Interface'] },
|
|
75
|
+
{ abbr: 'GUI', forms: ['图形用户界面', 'Graphical User Interface'] },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** W-26 汉字数字的计量、统计语境:数字 + 量词。单用「一」多属惯用语,不提示。 */
|
|
79
|
+
const W26_RE = /([二三四五六七八九十百千万亿两]+)(个|条|次|份|项|行|节|章|种|类|组|张|位|名|天|秒|分钟|小时|月|年|周|页|字符|步|层|处|款|列|人|件|套|遍|轮|档|期|段|句|字|倍|成|回|篇)/g;
|
|
80
|
+
/** 这些字在前表示序数、每一、约数或指代,不按计量数字提示。 */
|
|
81
|
+
const W26_SKIP_PREV = /[第每这那同约数几上下唯任某另前后首初末本该此其]/;
|
|
82
|
+
|
|
83
|
+
/** W-38 版本说明的标准分组,以及混用的近义写法。 */
|
|
84
|
+
const W38_GROUPS = ['新增', '变更', '修复', '移除', '安全'];
|
|
85
|
+
const W38_CHANGE_LIKE = /新增|变更|改动|修正|修复|移除|删除|安全|优化|调整|增强|特性/;
|
|
86
|
+
|
|
87
|
+
/** 条款清单:编号、级别与顺序。 */
|
|
88
|
+
const RULES = [
|
|
89
|
+
{ id: 'W-01', level: 'block' },
|
|
90
|
+
{ id: 'W-02', level: 'block' },
|
|
91
|
+
{ id: 'W-04', level: 'block' },
|
|
92
|
+
{ id: 'W-05', level: 'block' },
|
|
93
|
+
{ id: 'W-07', level: 'block' },
|
|
94
|
+
{ id: 'W-08', level: 'block' },
|
|
95
|
+
{ id: 'W-09', level: 'warn' },
|
|
96
|
+
{ id: 'W-19', level: 'warn' },
|
|
97
|
+
{ id: 'W-26', level: 'warn' },
|
|
98
|
+
{ id: 'W-38', level: 'warn' },
|
|
99
|
+
];
|
|
100
|
+
const RULE_INDEX = new Map(RULES.map((r, i) => [r.id, i]));
|
|
101
|
+
|
|
102
|
+
/* ============================== 工具函数 ============================== */
|
|
103
|
+
|
|
104
|
+
const CJK_RE = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/;
|
|
105
|
+
|
|
106
|
+
/** 取脚本所在仓库根:脚本位于 <root>/scripts/style-gate.mjs。 */
|
|
107
|
+
function repoRoot() {
|
|
108
|
+
const p = process.argv[1] ? path.resolve(process.argv[1]) : path.join(process.cwd(), 'scripts', 'style-gate.mjs');
|
|
109
|
+
return path.resolve(path.dirname(p), '..');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 递归列出目录下文件;先排序,保证输出稳定。 */
|
|
113
|
+
function walk(dir, accept, out) {
|
|
114
|
+
if (!fs.existsSync(dir)) return out;
|
|
115
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
116
|
+
const abs = path.join(dir, name);
|
|
117
|
+
let st;
|
|
118
|
+
try {
|
|
119
|
+
st = fs.statSync(abs);
|
|
120
|
+
} catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (st.isDirectory()) walk(abs, accept, out);
|
|
124
|
+
else if (accept(abs)) out.push(abs);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 是否属于排除的规范性文本目录。 */
|
|
130
|
+
function isSkipped(rel) {
|
|
131
|
+
return SKIP_DIRS.some((d) => rel === d || rel.startsWith(d + '/'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 收集本次扫描的文件清单。 */
|
|
135
|
+
function collectFiles(root) {
|
|
136
|
+
const files = [];
|
|
137
|
+
const readme = path.join(root, 'README.md');
|
|
138
|
+
if (fs.existsSync(readme)) files.push(readme);
|
|
139
|
+
const docsDir = path.join(root, 'docs');
|
|
140
|
+
walk(docsDir, (abs) => abs.toLowerCase().endsWith('.md'), files);
|
|
141
|
+
const templatesDir = path.join(root, 'src', 'templates');
|
|
142
|
+
walk(templatesDir, (abs) => !isSkipped(path.relative(root, abs).split(path.sep).join('/')), files);
|
|
143
|
+
const srcDir = path.join(root, 'src');
|
|
144
|
+
walk(srcDir, (abs) => {
|
|
145
|
+
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
146
|
+
return rel === 'src/client.js' || abs.toLowerCase().endsWith('.mjs');
|
|
147
|
+
}, files);
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const f of files) {
|
|
151
|
+
const rel = path.relative(root, f).split(path.sep).join('/');
|
|
152
|
+
if (isSkipped(rel) || seen.has(rel)) continue;
|
|
153
|
+
seen.add(rel);
|
|
154
|
+
out.push({ abs: f, rel });
|
|
155
|
+
}
|
|
156
|
+
return out.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 长度上限内截断,便于单行输出。 */
|
|
160
|
+
function clip(s, n) {
|
|
161
|
+
const t = String(s).replace(/\s+/g, ' ').trim();
|
|
162
|
+
return t.length > n ? t.slice(0, n) + '…' : t;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 去掉 markdown 行内代码、图片、链接目标与 HTML 标签;只留文字。 */
|
|
166
|
+
function stripMarkdown(text) {
|
|
167
|
+
let s = text;
|
|
168
|
+
s = s.replace(/`+[^`]*`+/g, ' '); // 行内代码段不参与判定
|
|
169
|
+
s = s.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ');
|
|
170
|
+
s = s.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1');
|
|
171
|
+
s = s.replace(/<[^>]+>/g, ' ');
|
|
172
|
+
s = s.replace(/https?:\/\/\S+/g, ' ');
|
|
173
|
+
return s;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 句长口径:汉字每字 1,英文字母与数字每 2 字符约合 1 字;标点不计。 */
|
|
177
|
+
function sentenceLength(s) {
|
|
178
|
+
let cjk = 0;
|
|
179
|
+
let ascii = 0;
|
|
180
|
+
for (const ch of s) {
|
|
181
|
+
if (CJK_RE.test(ch)) cjk++;
|
|
182
|
+
else if (/[A-Za-z0-9]/.test(ch)) ascii++;
|
|
183
|
+
}
|
|
184
|
+
return cjk + Math.ceil(ascii / 2);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 切句:按中文句末标点。返回句子数组。 */
|
|
188
|
+
function splitSentences(text) {
|
|
189
|
+
return text
|
|
190
|
+
.split(/[。!?!?]+/)
|
|
191
|
+
.map((s) => s.trim())
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 计算单句长度用的清洗:去掉 URL、长路径类词与 markdown 标记。 */
|
|
196
|
+
function cleanForLength(text) {
|
|
197
|
+
return text
|
|
198
|
+
.replace(/https?:\/\/\S+/g, ' ')
|
|
199
|
+
.split(/\s+/)
|
|
200
|
+
.map((tok) => (tok.length >= 12 && /[\/\\]/.test(tok) ? '' : tok)) // 长路径整词跳过
|
|
201
|
+
.join(' ')
|
|
202
|
+
.replace(/^\s*[#>*+-]+\s*/g, ' ')
|
|
203
|
+
.replace(/[*_~|]/g, ' ');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ============================== 抽取器 ============================== */
|
|
207
|
+
|
|
208
|
+
/** 抽取 markdown / 模板正文:跳过 fenced code block 与表格行(表格行只跳过 W-09)。 */
|
|
209
|
+
function extractMarkdown(abs) {
|
|
210
|
+
const src = fs.readFileSync(abs, 'utf8');
|
|
211
|
+
const lines = src.split(/\r?\n/);
|
|
212
|
+
const segs = [];
|
|
213
|
+
let fence = null;
|
|
214
|
+
for (let i = 0; i < lines.length; i++) {
|
|
215
|
+
const raw = lines[i];
|
|
216
|
+
const t = raw.trim();
|
|
217
|
+
const fenceMatch = t.match(/^(```+|~~~+)/);
|
|
218
|
+
if (fenceMatch) {
|
|
219
|
+
const mark = fenceMatch[1][0];
|
|
220
|
+
if (!fence) fence = mark;
|
|
221
|
+
else if (fence === mark) fence = null;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (fence) continue;
|
|
225
|
+
const text = stripMarkdown(raw);
|
|
226
|
+
if (!CJK_RE.test(text) && !W07_EMOJI_RE.test(text)) continue;
|
|
227
|
+
segs.push({ line: i + 1, text, raw, table: /^\s*\|/.test(raw) });
|
|
228
|
+
}
|
|
229
|
+
return segs;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 抽取 JS 源码:只取注释与字符串字面量里的中文(以及表情符号)。 */
|
|
233
|
+
function extractJs(abs) {
|
|
234
|
+
const src = fs.readFileSync(abs, 'utf8');
|
|
235
|
+
const segs = [];
|
|
236
|
+
const n = src.length;
|
|
237
|
+
let i = 0;
|
|
238
|
+
let line = 1;
|
|
239
|
+
let mode = 'code';
|
|
240
|
+
let buf = '';
|
|
241
|
+
let bufLine = 1;
|
|
242
|
+
let lastSig = '';
|
|
243
|
+
|
|
244
|
+
const push = (text, startLine, isComment) => {
|
|
245
|
+
if (!text) return;
|
|
246
|
+
const parts = text.split('\n');
|
|
247
|
+
for (let k = 0; k < parts.length; k++) {
|
|
248
|
+
let t = parts[k].replace(/\r$/, '');
|
|
249
|
+
if (isComment) t = t.replace(/^\s*\*+\s?/, '');
|
|
250
|
+
t = stripMarkdown(t);
|
|
251
|
+
if (!CJK_RE.test(t) && !W07_EMOJI_RE.test(t)) continue;
|
|
252
|
+
segs.push({ line: startLine + k, text: t, raw: t, table: false });
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// 跳过正则字面量:只在明显是正则的位置启用,避免把注释起点误判。
|
|
257
|
+
const skipRegex = (from) => {
|
|
258
|
+
let j = from + 1;
|
|
259
|
+
let inClass = false;
|
|
260
|
+
while (j < n) {
|
|
261
|
+
const c = src[j];
|
|
262
|
+
if (c === '\\') {
|
|
263
|
+
j += 2;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (c === '\n') return -1;
|
|
267
|
+
if (c === '[') inClass = true;
|
|
268
|
+
else if (c === ']') inClass = false;
|
|
269
|
+
else if (c === '/' && !inClass) {
|
|
270
|
+
j++;
|
|
271
|
+
while (j < n && /[a-z]/i.test(src[j])) j++;
|
|
272
|
+
return j;
|
|
273
|
+
}
|
|
274
|
+
j++;
|
|
275
|
+
}
|
|
276
|
+
return -1;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
while (i < n) {
|
|
280
|
+
const ch = src[i];
|
|
281
|
+
const next = src[i + 1];
|
|
282
|
+
if (mode === 'code') {
|
|
283
|
+
if (ch === '/' && next === '/') {
|
|
284
|
+
mode = 'line';
|
|
285
|
+
buf = '';
|
|
286
|
+
bufLine = line;
|
|
287
|
+
i += 2;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (ch === '/' && next === '*') {
|
|
291
|
+
mode = 'block';
|
|
292
|
+
buf = '';
|
|
293
|
+
bufLine = line;
|
|
294
|
+
i += 2;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
298
|
+
mode = ch === "'" ? 'sq' : ch === '"' ? 'dq' : 'tpl';
|
|
299
|
+
buf = '';
|
|
300
|
+
bufLine = line;
|
|
301
|
+
i++;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (ch === '/' && /[=(:,!&|?{};[+\-*%<>~^]/.test(lastSig)) {
|
|
305
|
+
const j = skipRegex(i);
|
|
306
|
+
if (j > 0) {
|
|
307
|
+
lastSig = '/';
|
|
308
|
+
i = j;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (ch === '\n') line++;
|
|
313
|
+
if (!/\s/.test(ch)) lastSig = ch;
|
|
314
|
+
i++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
// 注释与字符串内部
|
|
318
|
+
if ((mode === 'sq' || mode === 'dq' || mode === 'tpl') && ch === '\\') {
|
|
319
|
+
const nxt = src[i + 1];
|
|
320
|
+
if (nxt !== undefined) {
|
|
321
|
+
if (nxt === '\n') line++;
|
|
322
|
+
else buf += nxt === 'n' || nxt === 't' ? ' ' : nxt;
|
|
323
|
+
}
|
|
324
|
+
i += 2;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (mode === 'line' && ch === '\n') {
|
|
328
|
+
push(buf, bufLine, true);
|
|
329
|
+
mode = 'code';
|
|
330
|
+
buf = '';
|
|
331
|
+
line++;
|
|
332
|
+
lastSig = '';
|
|
333
|
+
i++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (mode === 'block' && ch === '*' && next === '/') {
|
|
337
|
+
push(buf, bufLine, true);
|
|
338
|
+
mode = 'code';
|
|
339
|
+
buf = '';
|
|
340
|
+
i += 2;
|
|
341
|
+
lastSig = '/';
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (mode === 'sq' && ch === "'") {
|
|
345
|
+
push(buf, bufLine, false);
|
|
346
|
+
mode = 'code';
|
|
347
|
+
buf = '';
|
|
348
|
+
i++;
|
|
349
|
+
lastSig = "'";
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (mode === 'dq' && ch === '"') {
|
|
353
|
+
push(buf, bufLine, false);
|
|
354
|
+
mode = 'code';
|
|
355
|
+
buf = '';
|
|
356
|
+
i++;
|
|
357
|
+
lastSig = '"';
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (mode === 'tpl' && ch === '`') {
|
|
361
|
+
push(buf, bufLine, false);
|
|
362
|
+
mode = 'code';
|
|
363
|
+
buf = '';
|
|
364
|
+
i++;
|
|
365
|
+
lastSig = '`';
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (ch === '\n') {
|
|
369
|
+
buf += ch;
|
|
370
|
+
line++;
|
|
371
|
+
i++;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
buf += ch;
|
|
375
|
+
i++;
|
|
376
|
+
}
|
|
377
|
+
if (mode !== 'code') push(buf, bufLine, mode === 'line' || mode === 'block');
|
|
378
|
+
return segs;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* ============================== 判定 ============================== */
|
|
382
|
+
|
|
383
|
+
/** 在一段文字里找词表命中,返回 [{ word, at }]。 */
|
|
384
|
+
function wordHits(text, words) {
|
|
385
|
+
const out = [];
|
|
386
|
+
for (const w of words) {
|
|
387
|
+
let from = 0;
|
|
388
|
+
for (;;) {
|
|
389
|
+
const at = text.indexOf(w, from);
|
|
390
|
+
if (at === -1) break;
|
|
391
|
+
out.push({ word: w, at });
|
|
392
|
+
from = at + w.length;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** 逐段判定阻断级条款(W-01、W-02、W-04、W-05、W-07、W-08)。 */
|
|
399
|
+
function scanBlockRules(rel, seg, add) {
|
|
400
|
+
// E-01:README.md「为什么需要」一节的指定句豁免 W-01(按节绑定,不按行号;该节正文由 check-docs 逐字比对)。
|
|
401
|
+
const e01 = rel === 'README.md' && String(seg.raw || '').indexOf(E01_TEXT) !== -1;
|
|
402
|
+
if (!e01) {
|
|
403
|
+
for (const h of wordHits(seg.text, W01_WORDS)) {
|
|
404
|
+
if (!isW01Word(seg.text, h.at, h.word)) continue;
|
|
405
|
+
add('W-01', '「' + h.word + '」');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const h of wordHits(seg.text, W02_WORDS)) add('W-02', '「' + h.word + '」');
|
|
409
|
+
for (const h of wordHits(seg.text, W04_WORDS)) add('W-04', '「' + h.word + '」');
|
|
410
|
+
|
|
411
|
+
const m5 = W05_RE.exec(seg.text);
|
|
412
|
+
if (m5) add('W-05', '「点」应写作「点击」');
|
|
413
|
+
|
|
414
|
+
const emoji = seg.text.match(W07_EMOJI_RE);
|
|
415
|
+
if (emoji) add('W-07', '表情符号「' + emoji[0] + '」');
|
|
416
|
+
for (const w of W07_NET_ABBR) {
|
|
417
|
+
if (new RegExp('\\b' + w + '\\b', 'i').test(seg.text)) add('W-07', '网络缩略语「' + w + '」');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// W-08:全角感叹号一律算;半角仅在中文语境中算,且排除 != 与 !==。
|
|
421
|
+
for (let i = 0; i < seg.text.length; i++) {
|
|
422
|
+
const ch = seg.text[i];
|
|
423
|
+
if (ch === '!') {
|
|
424
|
+
add('W-08', '感叹号「!」');
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
if (ch === '!') {
|
|
428
|
+
if (seg.text[i + 1] === '=') continue;
|
|
429
|
+
const prev = i > 0 ? seg.text[i - 1] : '';
|
|
430
|
+
const nxt = seg.text[i + 1] || '';
|
|
431
|
+
if (CJK_RE.test(prev) || CJK_RE.test(nxt)) {
|
|
432
|
+
add('W-08', '感叹号「!」');
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** W-09:单句超过上限。表格、代码块、长路径均不参与统计。 */
|
|
440
|
+
function scanW09(seg, add) {
|
|
441
|
+
if (seg.table) return;
|
|
442
|
+
for (const sentence of splitSentences(cleanForLength(seg.text))) {
|
|
443
|
+
const len = sentenceLength(sentence);
|
|
444
|
+
if (len > W09_LIMIT) {
|
|
445
|
+
add('W-09', len + ' 字(上限 ' + W09_LIMIT + ' 字):「' + clip(sentence, 24) + '」');
|
|
446
|
+
return; // 一行只报一次,避免同一行多句重复刷屏
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** W-26:汉字数字用于计量、统计、编号时提示。 */
|
|
452
|
+
function scanW26(seg, add) {
|
|
453
|
+
W26_RE.lastIndex = 0;
|
|
454
|
+
let m;
|
|
455
|
+
while ((m = W26_RE.exec(seg.text)) !== null) {
|
|
456
|
+
const at = m.index;
|
|
457
|
+
const prev = at > 0 ? seg.text[at - 1] : '';
|
|
458
|
+
// 万、千、百、亿作量级单位(如「1.1 万字符」)时不按汉字数字计数。
|
|
459
|
+
if (/^[万千百亿]$/.test(m[1])) continue;
|
|
460
|
+
// 紧跟数字或字母的是数值的一部分(如「3 万」「第2 条」),不判。
|
|
461
|
+
if (prev && /[0-9A-Za-z]/.test(prev)) continue;
|
|
462
|
+
if (prev && W26_SKIP_PREV.test(prev)) continue;
|
|
463
|
+
add('W-26', '「' + m[0] + '」(计量、统计、编号宜用阿拉伯数字)');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** W-19:同一文件内缩写首次出现未先写全称。只看首次出现,后文补写全称也不免除。 */
|
|
468
|
+
function scanW19(segs, add) {
|
|
469
|
+
for (const item of W19_ABBR) {
|
|
470
|
+
const re = new RegExp('\\b' + item.abbr + '\\b');
|
|
471
|
+
let prior = '';
|
|
472
|
+
for (const seg of segs) {
|
|
473
|
+
const m = re.exec(seg.text);
|
|
474
|
+
if (m) {
|
|
475
|
+
const before = prior + '\n' + seg.text.slice(0, m.index);
|
|
476
|
+
if (!item.forms.some((f) => before.indexOf(f) !== -1)) {
|
|
477
|
+
add('W-19', item.abbr + ' 首次出现未写全称(先写「' + item.forms[0] + '(' + item.abbr + ')」)', seg.line);
|
|
478
|
+
}
|
|
479
|
+
break; // 只看首次出现
|
|
480
|
+
}
|
|
481
|
+
prior += '\n' + seg.text;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** W-38:版本说明按新增、变更、修复、移除、安全分组。 */
|
|
487
|
+
function scanW38(abs, add) {
|
|
488
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
489
|
+
const headings = [];
|
|
490
|
+
text.split(/\r?\n/).forEach((lineText, i) => {
|
|
491
|
+
const m = lineText.match(/^##\s+(.+?)\s*$/);
|
|
492
|
+
if (m) headings.push({ line: i + 1, title: m[1].trim() });
|
|
493
|
+
});
|
|
494
|
+
const exact = headings.filter((h) => W38_GROUPS.indexOf(h.title) !== -1);
|
|
495
|
+
if (exact.length === 0) {
|
|
496
|
+
const line = headings.length ? headings[0].line : 1;
|
|
497
|
+
add('W-38', '未按 新增、变更、修复、移除、安全 分组', line);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
for (const h of headings) {
|
|
501
|
+
if (W38_GROUPS.indexOf(h.title) !== -1) continue;
|
|
502
|
+
if (!W38_CHANGE_LIKE.test(h.title)) continue;
|
|
503
|
+
add('W-38', '分组「' + h.title + '」应改用 新增、变更、修复、移除、安全', h.line);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* ============================== 主流程 ============================== */
|
|
508
|
+
|
|
509
|
+
function main() {
|
|
510
|
+
const root = repoRoot();
|
|
511
|
+
const files = collectFiles(root);
|
|
512
|
+
const findings = [];
|
|
513
|
+
const order = new Map(files.map((f, i) => [f.rel, i]));
|
|
514
|
+
|
|
515
|
+
for (const { abs, rel } of files) {
|
|
516
|
+
const isDoc = rel.startsWith('src/templates/') || rel.toLowerCase().endsWith('.md');
|
|
517
|
+
const segs = isDoc ? extractMarkdown(abs) : extractJs(abs);
|
|
518
|
+
const add = (rule, content, lineOverride) => {
|
|
519
|
+
findings.push({ rel, line: lineOverride || segLine, rule, content });
|
|
520
|
+
};
|
|
521
|
+
let segLine = 1;
|
|
522
|
+
|
|
523
|
+
for (const seg of segs) {
|
|
524
|
+
segLine = seg.line;
|
|
525
|
+
scanBlockRules(rel, seg, add);
|
|
526
|
+
scanW09(seg, add);
|
|
527
|
+
scanW26(seg, add);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 文件级条款
|
|
531
|
+
const fileAdd = (rule, content, line) => findings.push({ rel, line: line || 1, rule, content });
|
|
532
|
+
if (/^docs\/releases\/[^/]+\.md$/.test(rel)) scanW38(abs, fileAdd);
|
|
533
|
+
// W-19 只判文档;代码注释不成文,逐文件要求缩写全称会大面积误报。
|
|
534
|
+
if (rel === 'README.md' || rel.startsWith('docs/') || rel.startsWith('src/templates/')) {
|
|
535
|
+
scanW19(segs, fileAdd);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 去重并按「文件、行、条款」排序
|
|
540
|
+
const seen = new Set();
|
|
541
|
+
const list = [];
|
|
542
|
+
for (const f of findings) {
|
|
543
|
+
const key = f.rel + '\u0000' + f.line + '\u0000' + f.rule + '\u0000' + f.content;
|
|
544
|
+
if (seen.has(key)) continue;
|
|
545
|
+
seen.add(key);
|
|
546
|
+
list.push(f);
|
|
547
|
+
}
|
|
548
|
+
list.sort((a, b) => {
|
|
549
|
+
const fa = order.get(a.rel) || 0;
|
|
550
|
+
const fb = order.get(b.rel) || 0;
|
|
551
|
+
if (fa !== fb) return fa - fb;
|
|
552
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
553
|
+
return (RULE_INDEX.get(a.rule) || 0) - (RULE_INDEX.get(b.rule) || 0);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
if (list.length === 0) {
|
|
557
|
+
console.log('STYLE-GATE OK(' + files.length + ' 份文件,' + RULES.length + ' 条规则)');
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let blocking = 0;
|
|
562
|
+
for (const f of list) {
|
|
563
|
+
if (f.rule === 'W-01' || f.rule === 'W-02' || f.rule === 'W-04' || f.rule === 'W-05' || f.rule === 'W-07' || f.rule === 'W-08') blocking++;
|
|
564
|
+
}
|
|
565
|
+
// 只有警告时判通过与退出码 0,但逐条列出,供评审与后续清理。
|
|
566
|
+
console.log((blocking > 0 ? 'STYLE-GATE FAIL ' : 'STYLE-GATE PASS(无阻断,') + (blocking > 0 ? list.length + ' 项:' : list.length + ' 项警告):'));
|
|
567
|
+
for (const f of list) console.log(f.rel + ':' + f.line + ' ' + f.rule + ' ' + f.content);
|
|
568
|
+
process.exitCode = blocking > 0 ? 1 : 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
main();
|