@chengzhao-dev/dsh-cpp-workflow 0.5.4 → 0.6.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.en.md CHANGED
@@ -19,17 +19,21 @@ The exception material follows the C++ Core Guidelines' Error handling section a
19
19
  Pinned npm install:
20
20
 
21
21
  ```bash
22
- dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.5.4
22
+ dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.0
23
23
  ```
24
24
 
25
25
  The matching Git tag:
26
26
 
27
27
  ```bash
28
- dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.5.4
28
+ dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.0
29
29
  ```
30
30
 
31
31
  The desktop app owns the `desktop` profile. If the command-line `add` is rejected, add the plugin in the app settings and use the same pinned npm version. A Git tag install uses the prebuilt `lib/` committed in the repository and runs no build on the target machine; the package has no install script that needs approval.
32
32
 
33
+ ### Settings
34
+
35
+ The plugin page and the "C++ Workflow" section under Settings share one page for file-header tags: `@file` is always the file name, `@brief` is written by default, and `@author`, `@date`, `@copyright`, `@version` stay off and are written only when they hold a non-empty value; `@param`, `@return`, `@throws` are required by default. The preview follows the switches, saving takes effect on the next skill read, and existing sources are never rewritten.
36
+
33
37
  ## Workspace gate
34
38
 
35
39
  Only one condition must hold before either skill appears: walking up from the current directory to the Git root, the path contains `.agents/skills/cpp-workflow-project/SKILL.md`.
@@ -103,9 +107,11 @@ npm ci
103
107
  npm test # build, three-skill structural check, gate/environment/schema/comment tests
104
108
  npm run test:matrix # real build matrix: 4 templates x 5 standards
105
109
  npm run pack:check # inspect the npm package contents
110
+ npm run ci # replay the Ubuntu CI job: install, test, package check
111
+ npm run ci:full # ci plus the standard matrix; needs a POSIX host
106
112
  ```
107
113
 
108
- Source lives in `src/`; `npm run build` writes `lib/`. The structural check is `scripts/checking-skill-links.mjs`. Tests do not invoke a local compiler; the environment checks use fixed version strings. The standard matrix needs `cmake`, `ninja`, `perl`, and `clang++` (or the compiler named by `CXX`) on a POSIX host such as WSL, Linux, or macOS.
114
+ Source lives in `src/`; `npm run build` writes `lib/`. The structural check is `scripts/checking-skill-links.mjs`. Tests do not invoke a local compiler; the environment checks use fixed version strings. The standard matrix needs `cmake`, `ninja`, `perl`, and `clang++` (or the compiler named by `CXX`) on a POSIX host such as WSL, Linux, or macOS. On Windows, run `npm run ci` first, then run `npm run ci:full` from WSL inside the checkout.
109
115
 
110
116
  ## License
111
117
 
package/README.md CHANGED
@@ -19,17 +19,21 @@
19
19
  npm 安装钉死版本:
20
20
 
21
21
  ```bash
22
- dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.5.4
22
+ dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.0
23
23
  ```
24
24
 
25
25
  同版本的 Git 标签:
26
26
 
27
27
  ```bash
28
- dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.5.4
28
+ dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.0
29
29
  ```
30
30
 
31
31
  桌面应用独占 `desktop` profile。命令行 `add` 被拒绝时,在应用设置里添加插件,地址填同一个 npm 版本。Git 标签安装使用仓库内已构建的 `lib/`,不在目标机器上重新构建;包内也不含需要授权执行的安装脚本。
32
32
 
33
+ ### 设置
34
+
35
+ 插件页与设置导航里的「C++ 工程流程」共用一页,只管文件头标签:`@file` 固定为文件名,`@brief` 默认写出,`@author`、`@date`、`@copyright`、`@version` 默认关闭且只在填了非空值时才写出;`@param`、`@return`、`@throws` 默认要求。预览随开关即时更新,保存后下一次读取技能即生效,已有源码不会被改写。
36
+
33
37
  ## 工作区门禁
34
38
 
35
39
  一个会话里只需要满足一件事,技能就会出现:从当前目录向上、停在 git 根,沿途有 `.agents/skills/cpp-workflow-project/SKILL.md`。
@@ -103,9 +107,11 @@ npm ci
103
107
  npm test # 构建、三技能结构检查、门禁/环境/schema/注释测试
104
108
  npm run test:matrix # 四套模板 × 五档标准的真实构建矩阵
105
109
  npm run pack:check # 核对 npm 包内容
110
+ npm run ci # 复现 Ubuntu CI:安装、测试、打包检查
111
+ npm run ci:full # 在 ci 之上再跑标准矩阵,需 POSIX 宿主
106
112
  ```
107
113
 
108
- 源码在 `src/`,`npm run build` 生成 `lib/`。结构检查在 `scripts/checking-skill-links.mjs`;测试不调用本机编译器,环境检查用固定版本字符串。标准矩阵需要 POSIX 宿主上的 `cmake`、`ninja`、`perl` 和 `clang++`(或 `CXX` 指定的编译器),在 WSL/Linux/macOS 上运行。
114
+ 源码在 `src/`,`npm run build` 生成 `lib/`。结构检查在 `scripts/checking-skill-links.mjs`;测试不调用本机编译器,环境检查用固定版本字符串。标准矩阵需要 POSIX 宿主上的 `cmake`、`ninja`、`perl` 和 `clang++`(或 `CXX` 指定的编译器),在 WSL/Linux/macOS 上运行。Windows 上先跑 `npm run ci`,再从 WSL 进入仓库跑 `npm run ci:full`。
109
115
 
110
116
  ## 许可
111
117
 
package/lib/client.js ADDED
@@ -0,0 +1,570 @@
1
+ // 由 scripts/building-client.mjs 生成,请改 src/comment-policy.ts 与 src/client/app.cjs.js。
2
+ window.__ModuleLoader__.load({
3
+ id: "@chengzhao-dev/dsh-cpp-workflow",
4
+ factory: (require) => {
5
+ var module = { exports: {} }
6
+ var exports = module.exports
7
+
8
+ /**
9
+ * 注释标签策略:设置页的预览、技能快照与检查器参数共用这一份。
10
+ *
11
+ * 配置只描述「写注释时默认带哪些标签」,不描述写法本身;写法在
12
+ * `skills/developing-cpp/references/comments.md`。设置页保存的值由宿主
13
+ * 写回插件 Config,服务端 `apply(ctx, config)` 用它生成本页快照与检查器参数。
14
+ *
15
+ * 这份文件被 `tsc` 编进 `lib/comment-policy.js`,打包客户端半侧时再内联进
16
+ * `lib/client.js`,所以只能依赖纯函数,不要引入 Node 或 React。
17
+ */
18
+ /** 文件头标签。 */
19
+ const HEADER_TAGS = ['brief', 'author', 'date', 'copyright', 'version'];
20
+ /** 函数契约标签:开关决定是否要求写出。 */
21
+ const CONTRACT_TAGS = ['param', 'returns', 'throws'];
22
+ /** 折叠在「更多标签」里的可选标签:默认关闭。 */
23
+ const EXTRA_TAGS = ['note', 'see', 'pre', 'post', 'tparam', 'retval'];
24
+ /** 只有用户主动填了非空值才放行的隐私标签。 */
25
+ const PRIVACY_TAGS = ['author', 'date', 'copyright', 'version'];
26
+ /** 标签说明:设置页逐行显示,用作行的辅助文字。 */
27
+ const TAG_TEXT = {
28
+ brief: '一句话说明这个文件定义或实现什么',
29
+ author: '只在用户填了非空值时写出',
30
+ date: '只在用户填了非空值时写出',
31
+ copyright: '只在用户填了非空值时写出',
32
+ version: '只在用户填了非空值时写出',
33
+ param: '函数参数的含义、范围或所有权',
34
+ returns: '返回值的条件;返回类型自明时可以省略',
35
+ throws: '抛出的异常类型与条件;不抛异常的函数不写',
36
+ note: '调用方需要注意的补充说明',
37
+ see: '相关函数、类型或文档',
38
+ pre: '调用前必须成立的条件',
39
+ post: '调用后保证成立的条件',
40
+ tparam: '模板参数',
41
+ retval: '特定返回值的含义',
42
+ };
43
+ /** 开关型标签在设置页的标题。 */
44
+ const TAG_LABEL = {
45
+ brief: '@brief',
46
+ author: '@author',
47
+ date: '@date',
48
+ copyright: '@copyright',
49
+ version: '@version',
50
+ param: '@param',
51
+ returns: '@return',
52
+ throws: '@throws',
53
+ note: '@note',
54
+ see: '@see',
55
+ pre: '@pre',
56
+ post: '@post',
57
+ tparam: '@tparam',
58
+ retval: '@retval',
59
+ };
60
+ /** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
61
+ const DEFAULT_CONFIG = {
62
+ brief: { enabled: true, value: '' },
63
+ author: { enabled: false, value: '' },
64
+ date: { enabled: false, value: '' },
65
+ copyright: { enabled: false, value: '' },
66
+ version: { enabled: false, value: '' },
67
+ param: true,
68
+ returns: true,
69
+ throws: true,
70
+ note: false,
71
+ see: false,
72
+ pre: false,
73
+ post: false,
74
+ tparam: false,
75
+ retval: false,
76
+ };
77
+ function text(value) {
78
+ return typeof value === 'string' ? value.trim() : '';
79
+ }
80
+ function flag(value, fallback) {
81
+ return typeof value === 'boolean' ? value : fallback;
82
+ }
83
+ /**
84
+ * 把宿主给的值补齐成完整配置;缺字段、旧版本或手工改坏的配置都回到出厂值。
85
+ */
86
+ function normalizeConfig(raw) {
87
+ const source = raw && typeof raw === 'object' ? raw : {};
88
+ const config = {};
89
+ for (const tag of HEADER_TAGS) {
90
+ const entry = source[tag] && typeof source[tag] === 'object' ? source[tag] : {};
91
+ config[tag] = {
92
+ enabled: flag(entry.enabled, DEFAULT_CONFIG[tag].enabled),
93
+ value: text(entry.value),
94
+ };
95
+ }
96
+ for (const tag of [...CONTRACT_TAGS, ...EXTRA_TAGS]) {
97
+ config[tag] = flag(source[tag], DEFAULT_CONFIG[tag]);
98
+ }
99
+ return config;
100
+ }
101
+ /**
102
+ * 按配置渲染文件头;`@file` 始终是传入的文件名。
103
+ */
104
+ function renderFileHeader(raw, fileName) {
105
+ const config = normalizeConfig(raw);
106
+ const name = text(fileName) || 'board.h';
107
+ const lines = [`/// @file ${name}`];
108
+ if (config.brief.enabled) {
109
+ lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
110
+ }
111
+ for (const tag of ['author', 'date', 'copyright', 'version']) {
112
+ if (config[tag].enabled && config[tag].value)
113
+ lines.push(`/// @${tag} ${config[tag].value}`);
114
+ }
115
+ return lines;
116
+ }
117
+ /**
118
+ * 设置页的预览:文件头 + 一段函数注释,只显示当前配置会写出的标签。
119
+ */
120
+ function previewParts(raw, sampleName) {
121
+ const config = normalizeConfig(raw);
122
+ const header = renderFileHeader(config, sampleName || 'board.h');
123
+ const contract = [];
124
+ if (config.param)
125
+ contract.push('/// @param position 目标行列坐标。');
126
+ if (config.returns)
127
+ contract.push('/// @return 放置成功时为 true。');
128
+ if (config.throws)
129
+ contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
130
+ const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
131
+ return { header, contract, declaration };
132
+ }
133
+ /**
134
+ * 设置页预览的纯文本形式;客户端半侧用同一份实现渲染 `<pre>`。
135
+ */
136
+ function previewText(raw, sampleName) {
137
+ const { header, contract, declaration } = previewParts(raw, sampleName);
138
+ const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
139
+ if (contract.length > 0)
140
+ lines.push('///', ...contract);
141
+ lines.push(declaration);
142
+ return lines.join('\n');
143
+ }
144
+ /**
145
+ * 检查器参数:隐私标签要显式放行,@brief 被关掉时跳过摘要检查。
146
+ */
147
+ function checkerArgs(raw) {
148
+ const config = normalizeConfig(raw);
149
+ const args = [];
150
+ if (!config.brief.enabled)
151
+ args.push('--no-brief');
152
+ for (const tag of PRIVACY_TAGS) {
153
+ if (config[tag].enabled && config[tag].value)
154
+ args.push('--allow-tag', tag);
155
+ }
156
+ return args;
157
+ }
158
+ /**
159
+ * 技能快照:`get()` 把它接在技能正文末尾,让代理看到当前 profile 的标签开关。
160
+ */
161
+ function snapshotText(raw) {
162
+ const config = normalizeConfig(raw);
163
+ const on = [];
164
+ const off = [];
165
+ for (const tag of HEADER_TAGS) {
166
+ const entry = config[tag];
167
+ const label = entry.value ? `${TAG_LABEL[tag]} ${entry.value}` : TAG_LABEL[tag];
168
+ if (entry.enabled)
169
+ on.push(label);
170
+ else
171
+ off.push(TAG_LABEL[tag]);
172
+ }
173
+ const contract = CONTRACT_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
174
+ const extra = EXTRA_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
175
+ const allowed = PRIVACY_TAGS.filter((tag) => config[tag].enabled && config[tag].value);
176
+ const head = `## 当前注释配置(profile)\n\n`;
177
+ const body = [
178
+ `- 文件头:@file 固定为文件名;开启 ${on.join('、') || '无'};关闭 ${off.join('、') || '无'}。`,
179
+ `- 函数契约:${contract.length > 0 ? contract.join('、') : '不要求'}。`,
180
+ `- 更多标签:${extra.length > 0 ? `可选 ${extra.join('、')}` : '未启用'}。`,
181
+ `- 检查器放行:${allowed.length > 0 ? allowed.join('、') : '无'}(隐私标签只有填了非空值才放行)。`,
182
+ ];
183
+ return head + body.join('\n');
184
+ }
185
+
186
+
187
+ // 客户端半侧:设置导航里的「C++ 工程流程」页与插件页的配置卡片共用同一个组件。
188
+ // 标签策略在本文件上方内联(由 scripts/building-client.mjs 从 src/comment-policy.js
189
+ // 拼进来),所以预览、默认值与服务端快照永远同源;这里只做表单与排版。
190
+ const React = require('react')
191
+ const h = React.createElement
192
+
193
+ const PACKAGE_NAME = '@chengzhao-dev/dsh-cpp-workflow'
194
+ const SAMPLE_FILE = 'board.h'
195
+
196
+ const STRINGS = {
197
+ zh: {
198
+ nav: 'C++ 工程流程',
199
+ title: '文档注释',
200
+ intro: '文件头与函数契约默认写出哪些 Doxygen 标签。配置只影响代理写注释的默认值,不改已有源码。',
201
+ header: '文件头',
202
+ contract: '函数契约',
203
+ extra: '更多标签',
204
+ extraHint: '默认关闭;需要时在单个函数上补写。',
205
+ file: '@file',
206
+ fileHint: '固定为当前文件名,不参与配置。',
207
+ emptyValue: '留空则由每个文件自己写',
208
+ on: '写出',
209
+ off: '不写',
210
+ preview: '预览',
211
+ previewHint: '示例文件名 board.h;预览随开关即时更新。',
212
+ save: '保存',
213
+ reset: '重置',
214
+ saved: '已保存,下一次读取技能时生效。',
215
+ rejected: '宿主拒绝了这次保存:配置可能已在别处修改,请重置后再试。',
216
+ unsaved: '有未保存的改动。',
217
+ loading: '正在读取配置…',
218
+ unreadable: '当前账户不能修改这份配置。',
219
+ readOnly: '此处只显示出厂值。要修改配置,请到「插件」页打开 dsh-cpp-workflow 的配置卡片。',
220
+ noForm: '这个入口没有可写表单,配置在插件页修改。',
221
+ },
222
+ en: {
223
+ nav: 'C++ Workflow',
224
+ title: 'Documentation comments',
225
+ intro: 'Which Doxygen tags the agent writes by default. This changes defaults only; existing sources are untouched.',
226
+ header: 'File header',
227
+ contract: 'Function contract',
228
+ extra: 'More tags',
229
+ extraHint: 'Off by default; add them per function when needed.',
230
+ file: '@file',
231
+ fileHint: 'Always the current file name; not configurable.',
232
+ emptyValue: 'Leave empty and each file writes its own',
233
+ on: 'Write',
234
+ off: 'Omit',
235
+ preview: 'Preview',
236
+ previewHint: 'Sample name board.h; the preview follows the switches.',
237
+ save: 'Save',
238
+ reset: 'Reset',
239
+ saved: 'Saved; the next skill read uses it.',
240
+ rejected: 'The host rejected the save: the config changed elsewhere. Reset and try again.',
241
+ unsaved: 'Unsaved changes.',
242
+ loading: 'Reading configuration…',
243
+ unreadable: 'This account cannot edit the configuration.',
244
+ readOnly: 'Showing factory defaults. Edit the dsh-cpp-workflow card on the Plugins page.',
245
+ noForm: 'This entry has no writable form; edit it on the Plugins page.',
246
+ },
247
+ }
248
+
249
+ function language() {
250
+ const value = typeof navigator !== 'undefined' && navigator.language ? navigator.language : 'zh'
251
+ return String(value).toLowerCase().startsWith('zh') ? 'zh' : 'en'
252
+ }
253
+
254
+ function t() {
255
+ return STRINGS[language()]
256
+ }
257
+
258
+ const palette = {
259
+ muted: 'var(--dsw-text-tertiary, #6b7280)',
260
+ line: 'var(--dsw-border-subtle, rgba(127, 127, 127, 0.24))',
261
+ surface: 'var(--dsw-surface-raised, rgba(127, 127, 127, 0.06))',
262
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
263
+ }
264
+
265
+ function rowStyle(extra) {
266
+ return {
267
+ display: 'flex',
268
+ alignItems: 'center',
269
+ gap: 10,
270
+ padding: '7px 0',
271
+ borderBottom: `1px solid ${palette.line}`,
272
+ ...extra,
273
+ }
274
+ }
275
+
276
+ function hintStyle() {
277
+ return { margin: '2px 0 0', fontSize: 12, lineHeight: 1.5, color: palette.muted }
278
+ }
279
+
280
+ function Switch(props) {
281
+ return h(
282
+ 'label',
283
+ { style: { display: 'inline-flex', alignItems: 'center', gap: 8, cursor: props.disabled ? 'default' : 'pointer', minWidth: 132 } },
284
+ h('input', {
285
+ type: 'checkbox',
286
+ checked: Boolean(props.checked),
287
+ disabled: Boolean(props.disabled),
288
+ onChange: (event) => props.onChange(event.target.checked),
289
+ style: { margin: 0 },
290
+ }),
291
+ h('span', { style: { fontSize: 13, fontFamily: palette.mono } }, props.label),
292
+ )
293
+ }
294
+
295
+ function HeaderTagRow(props) {
296
+ const s = props.strings
297
+ const entry = props.entry
298
+ return h(
299
+ 'div',
300
+ { style: rowStyle({ flexWrap: 'wrap' }) },
301
+ h('div', { style: { flex: '1 1 280px', minWidth: 0 } },
302
+ h(Switch, {
303
+ label: TAG_LABEL[props.tag],
304
+ checked: entry.enabled,
305
+ onChange: (enabled) => props.onChange({ ...entry, enabled }),
306
+ }),
307
+ h('p', { style: hintStyle() }, TAG_TEXT[props.tag] || ''),
308
+ ),
309
+ h('input', {
310
+ type: 'text',
311
+ value: entry.value,
312
+ placeholder: s.emptyValue,
313
+ disabled: !entry.enabled,
314
+ 'aria-label': `${TAG_LABEL[props.tag]} value`,
315
+ onChange: (event) => props.onChange({ ...entry, value: event.target.value }),
316
+ style: {
317
+ flex: '1 1 220px',
318
+ minWidth: 0,
319
+ fontSize: 13,
320
+ padding: '5px 8px',
321
+ borderRadius: 6,
322
+ border: `1px solid ${palette.line}`,
323
+ background: entry.enabled ? 'transparent' : palette.surface,
324
+ color: 'inherit',
325
+ },
326
+ }),
327
+ )
328
+ }
329
+
330
+ function Preview(props) {
331
+ const s = props.strings
332
+ const text = previewText(props.config, SAMPLE_FILE)
333
+ return h(
334
+ 'section',
335
+ { 'aria-label': s.preview, style: { marginTop: 18 } },
336
+ h('h4', { style: { margin: '0 0 4px', fontSize: 13, fontWeight: 600 } }, s.preview),
337
+ h('p', { style: hintStyle() }, s.previewHint),
338
+ h(
339
+ 'pre',
340
+ {
341
+ style: {
342
+ margin: '8px 0 0',
343
+ padding: '10px 12px',
344
+ borderRadius: 8,
345
+ background: palette.surface,
346
+ border: `1px solid ${palette.line}`,
347
+ overflowX: 'auto',
348
+ fontSize: 12.5,
349
+ lineHeight: 1.65,
350
+ fontFamily: palette.mono,
351
+ whiteSpace: 'pre',
352
+ },
353
+ },
354
+ text,
355
+ ),
356
+ )
357
+ }
358
+
359
+ function diffOperations(draft, base) {
360
+ const operations = []
361
+ for (const key of Object.keys(DEFAULT_CONFIG)) {
362
+ const next = JSON.stringify(draft[key])
363
+ const previous = JSON.stringify(base[key])
364
+ if (next !== previous) operations.push({ op: 'set', path: [key], value: draft[key] })
365
+ }
366
+ return operations
367
+ }
368
+
369
+ function ConfigPage(props) {
370
+ const s = t()
371
+ const form = props.form
372
+ const initial = React.useMemo(() => normalizeConfig(form && form.state ? form.state.value : undefined), [])
373
+ const [draft, setDraft] = React.useState(initial)
374
+ const [base, setBase] = React.useState(initial)
375
+ const [dirty, setDirty] = React.useState(false)
376
+ const [busy, setBusy] = React.useState(false)
377
+ const [message, setMessage] = React.useState('')
378
+ const revision = form && form.state ? form.state.revision : undefined
379
+ const ready = Boolean(form && form.state && form.state.status === 'ready' && form.state.writable && form.state.mode === 'host')
380
+ const status = form && form.state ? form.state.status : 'missing'
381
+
382
+ React.useEffect(() => {
383
+ if (dirty || !form || !form.state || form.state.status !== 'ready') return
384
+ const next = normalizeConfig(form.state.value)
385
+ setBase(next)
386
+ setDraft(next)
387
+ }, [revision, status, dirty, form])
388
+
389
+ const update = (patch) => {
390
+ setDraft((current) => ({ ...current, ...patch }))
391
+ setDirty(true)
392
+ setMessage('')
393
+ }
394
+
395
+ const reset = () => {
396
+ setDraft(base)
397
+ setDirty(false)
398
+ setMessage('')
399
+ }
400
+
401
+ const save = async () => {
402
+ if (!ready || busy) return
403
+ const operations = diffOperations(draft, base)
404
+ if (operations.length === 0) return
405
+ setBusy(true)
406
+ try {
407
+ const accepted = await form.mutate(operations, revision)
408
+ if (accepted) {
409
+ setBase(draft)
410
+ setDirty(false)
411
+ setMessage(s.saved)
412
+ } else {
413
+ setMessage(s.rejected)
414
+ }
415
+ } catch (error) {
416
+ setMessage(error instanceof Error ? error.message : String(error))
417
+ } finally {
418
+ setBusy(false)
419
+ }
420
+ }
421
+
422
+ const editable = Boolean(form)
423
+ const editableReady = editable && ready
424
+ const headerTags = ['brief', 'author', 'date', 'copyright', 'version']
425
+
426
+ return h(
427
+ 'div',
428
+ { style: { display: 'flex', flexDirection: 'column', gap: 4, padding: '4px 0 12px', maxWidth: 720 } },
429
+ h('h3', { style: { margin: '0 0 2px', fontSize: 15, fontWeight: 600 } }, s.title),
430
+ h('p', { style: hintStyle() }, s.intro),
431
+ !editable
432
+ ? h('p', { role: 'status', style: { ...hintStyle(), color: 'inherit', marginTop: 8 } }, s.readOnly)
433
+ : null,
434
+ editable && !ready && status !== 'missing'
435
+ ? h('p', { role: 'status', style: hintStyle() }, status === 'loading' ? s.loading : s.unreadable)
436
+ : null,
437
+ h(
438
+ 'fieldset',
439
+ { style: { margin: '14px 0 0', padding: 0, border: 0 } },
440
+ h('legend', { style: { padding: 0, fontSize: 13, fontWeight: 600 } }, s.header),
441
+ h(
442
+ 'div',
443
+ { style: rowStyle() },
444
+ h(Switch, { label: s.file, checked: true, disabled: true, onChange: () => {} }),
445
+ h('p', { style: { ...hintStyle(), margin: 0 } }, s.fileHint),
446
+ ),
447
+ ...headerTags.map((tag) =>
448
+ h(HeaderTagRow, {
449
+ key: tag,
450
+ tag,
451
+ strings: s,
452
+ entry: draft[tag],
453
+ onChange: (entry) => update({ [tag]: entry }),
454
+ }),
455
+ ),
456
+ ),
457
+ h(
458
+ 'fieldset',
459
+ { style: { margin: '18px 0 0', padding: 0, border: 0 } },
460
+ h('legend', { style: { padding: 0, fontSize: 13, fontWeight: 600 } }, s.contract),
461
+ h(
462
+ 'div',
463
+ { style: { display: 'flex', flexWrap: 'wrap', gap: '6px 18px', padding: '8px 0 2px' } },
464
+ ...CONTRACT_TAGS.map((tag) =>
465
+ h(
466
+ 'div',
467
+ { key: tag, style: { flex: '1 1 180px', minWidth: 180 } },
468
+ h(Switch, {
469
+ label: TAG_LABEL[tag],
470
+ checked: draft[tag],
471
+ onChange: (value) => update({ [tag]: value }),
472
+ }),
473
+ h('p', { style: hintStyle() }, TAG_TEXT[tag]),
474
+ ),
475
+ ),
476
+ ),
477
+ ),
478
+ h(
479
+ 'details',
480
+ { style: { marginTop: 16, paddingTop: 10, borderTop: `1px solid ${palette.line}` } },
481
+ h('summary', { style: { cursor: 'pointer', fontSize: 13, fontWeight: 600 } }, s.extra),
482
+ h('p', { style: hintStyle() }, s.extraHint),
483
+ h(
484
+ 'div',
485
+ { style: { display: 'flex', flexWrap: 'wrap', gap: '6px 18px', paddingTop: 4 } },
486
+ ...EXTRA_TAGS.map((tag) =>
487
+ h(
488
+ 'div',
489
+ { key: tag, style: { flex: '1 1 180px', minWidth: 180 } },
490
+ h(Switch, {
491
+ label: TAG_LABEL[tag],
492
+ checked: draft[tag],
493
+ onChange: (value) => update({ [tag]: value }),
494
+ }),
495
+ ),
496
+ ),
497
+ ),
498
+ ),
499
+ h(Preview, { config: draft, strings: s }),
500
+ editable
501
+ ? h(
502
+ 'div',
503
+ { style: { display: 'flex', alignItems: 'center', gap: 10, marginTop: 16, flexWrap: 'wrap' } },
504
+ h(
505
+ 'button',
506
+ {
507
+ type: 'button',
508
+ onClick: save,
509
+ disabled: busy || !editableReady || !dirty,
510
+ style: { fontSize: 13, padding: '6px 14px', borderRadius: 6, cursor: 'pointer' },
511
+ },
512
+ s.save,
513
+ ),
514
+ h(
515
+ 'button',
516
+ {
517
+ type: 'button',
518
+ onClick: reset,
519
+ disabled: busy || !dirty,
520
+ style: { fontSize: 13, padding: '6px 14px', borderRadius: 6, cursor: 'pointer' },
521
+ },
522
+ s.reset,
523
+ ),
524
+ h('p', { role: 'status', style: { ...hintStyle(), margin: 0, flex: '1 1 220px' } }, message || (dirty ? s.unsaved : '')),
525
+ )
526
+ : null,
527
+ )
528
+ }
529
+
530
+ function page(ownerProps, titled) {
531
+ const props = ownerProps || {}
532
+ return h(ConfigPage, { ...props, titled })
533
+ }
534
+
535
+ function apply(ctx) {
536
+ ctx.slots.inject('settings.section', () =>
537
+ ctx.slots.register(
538
+ {
539
+ name: 'settings.section',
540
+ id: 'cpp-workflow',
541
+ order: 60,
542
+ label: () => t().nav,
543
+ },
544
+ (ownerProps) => page(ownerProps, true),
545
+ ),
546
+ )
547
+ ctx.slots.inject('plugins.bundle.config', () =>
548
+ ctx.slots.register(
549
+ {
550
+ name: 'plugins.bundle.config',
551
+ key: PACKAGE_NAME,
552
+ label: () => t().nav,
553
+ },
554
+ (ownerProps) => (ownerProps && ownerProps.view === 'summary' ? null : page(ownerProps, false)),
555
+ ),
556
+ )
557
+ }
558
+
559
+ module.exports = {
560
+ name: PACKAGE_NAME,
561
+ inject: ['slots'],
562
+ apply,
563
+ ConfigPage,
564
+ diffOperations,
565
+ previewText,
566
+ }
567
+
568
+ return module.exports
569
+ },
570
+ })
@@ -0,0 +1,177 @@
1
+ /**
2
+ * 注释标签策略:设置页的预览、技能快照与检查器参数共用这一份。
3
+ *
4
+ * 配置只描述「写注释时默认带哪些标签」,不描述写法本身;写法在
5
+ * `skills/developing-cpp/references/comments.md`。设置页保存的值由宿主
6
+ * 写回插件 Config,服务端 `apply(ctx, config)` 用它生成本页快照与检查器参数。
7
+ *
8
+ * 这份文件被 `tsc` 编进 `lib/comment-policy.js`,打包客户端半侧时再内联进
9
+ * `lib/client.js`,所以只能依赖纯函数,不要引入 Node 或 React。
10
+ */
11
+ /** 文件头标签。 */
12
+ export const HEADER_TAGS = ['brief', 'author', 'date', 'copyright', 'version'];
13
+ /** 函数契约标签:开关决定是否要求写出。 */
14
+ export const CONTRACT_TAGS = ['param', 'returns', 'throws'];
15
+ /** 折叠在「更多标签」里的可选标签:默认关闭。 */
16
+ export const EXTRA_TAGS = ['note', 'see', 'pre', 'post', 'tparam', 'retval'];
17
+ /** 只有用户主动填了非空值才放行的隐私标签。 */
18
+ export const PRIVACY_TAGS = ['author', 'date', 'copyright', 'version'];
19
+ /** 标签说明:设置页逐行显示,用作行的辅助文字。 */
20
+ export const TAG_TEXT = {
21
+ brief: '一句话说明这个文件定义或实现什么',
22
+ author: '只在用户填了非空值时写出',
23
+ date: '只在用户填了非空值时写出',
24
+ copyright: '只在用户填了非空值时写出',
25
+ version: '只在用户填了非空值时写出',
26
+ param: '函数参数的含义、范围或所有权',
27
+ returns: '返回值的条件;返回类型自明时可以省略',
28
+ throws: '抛出的异常类型与条件;不抛异常的函数不写',
29
+ note: '调用方需要注意的补充说明',
30
+ see: '相关函数、类型或文档',
31
+ pre: '调用前必须成立的条件',
32
+ post: '调用后保证成立的条件',
33
+ tparam: '模板参数',
34
+ retval: '特定返回值的含义',
35
+ };
36
+ /** 开关型标签在设置页的标题。 */
37
+ export const TAG_LABEL = {
38
+ brief: '@brief',
39
+ author: '@author',
40
+ date: '@date',
41
+ copyright: '@copyright',
42
+ version: '@version',
43
+ param: '@param',
44
+ returns: '@return',
45
+ throws: '@throws',
46
+ note: '@note',
47
+ see: '@see',
48
+ pre: '@pre',
49
+ post: '@post',
50
+ tparam: '@tparam',
51
+ retval: '@retval',
52
+ };
53
+ /** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
54
+ export const DEFAULT_CONFIG = {
55
+ brief: { enabled: true, value: '' },
56
+ author: { enabled: false, value: '' },
57
+ date: { enabled: false, value: '' },
58
+ copyright: { enabled: false, value: '' },
59
+ version: { enabled: false, value: '' },
60
+ param: true,
61
+ returns: true,
62
+ throws: true,
63
+ note: false,
64
+ see: false,
65
+ pre: false,
66
+ post: false,
67
+ tparam: false,
68
+ retval: false,
69
+ };
70
+ function text(value) {
71
+ return typeof value === 'string' ? value.trim() : '';
72
+ }
73
+ function flag(value, fallback) {
74
+ return typeof value === 'boolean' ? value : fallback;
75
+ }
76
+ /**
77
+ * 把宿主给的值补齐成完整配置;缺字段、旧版本或手工改坏的配置都回到出厂值。
78
+ */
79
+ export function normalizeConfig(raw) {
80
+ const source = raw && typeof raw === 'object' ? raw : {};
81
+ const config = {};
82
+ for (const tag of HEADER_TAGS) {
83
+ const entry = source[tag] && typeof source[tag] === 'object' ? source[tag] : {};
84
+ config[tag] = {
85
+ enabled: flag(entry.enabled, DEFAULT_CONFIG[tag].enabled),
86
+ value: text(entry.value),
87
+ };
88
+ }
89
+ for (const tag of [...CONTRACT_TAGS, ...EXTRA_TAGS]) {
90
+ config[tag] = flag(source[tag], DEFAULT_CONFIG[tag]);
91
+ }
92
+ return config;
93
+ }
94
+ /**
95
+ * 按配置渲染文件头;`@file` 始终是传入的文件名。
96
+ */
97
+ export function renderFileHeader(raw, fileName) {
98
+ const config = normalizeConfig(raw);
99
+ const name = text(fileName) || 'board.h';
100
+ const lines = [`/// @file ${name}`];
101
+ if (config.brief.enabled) {
102
+ lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
103
+ }
104
+ for (const tag of ['author', 'date', 'copyright', 'version']) {
105
+ if (config[tag].enabled && config[tag].value)
106
+ lines.push(`/// @${tag} ${config[tag].value}`);
107
+ }
108
+ return lines;
109
+ }
110
+ /**
111
+ * 设置页的预览:文件头 + 一段函数注释,只显示当前配置会写出的标签。
112
+ */
113
+ export function previewParts(raw, sampleName) {
114
+ const config = normalizeConfig(raw);
115
+ const header = renderFileHeader(config, sampleName || 'board.h');
116
+ const contract = [];
117
+ if (config.param)
118
+ contract.push('/// @param position 目标行列坐标。');
119
+ if (config.returns)
120
+ contract.push('/// @return 放置成功时为 true。');
121
+ if (config.throws)
122
+ contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
123
+ const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
124
+ return { header, contract, declaration };
125
+ }
126
+ /**
127
+ * 设置页预览的纯文本形式;客户端半侧用同一份实现渲染 `<pre>`。
128
+ */
129
+ export function previewText(raw, sampleName) {
130
+ const { header, contract, declaration } = previewParts(raw, sampleName);
131
+ const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
132
+ if (contract.length > 0)
133
+ lines.push('///', ...contract);
134
+ lines.push(declaration);
135
+ return lines.join('\n');
136
+ }
137
+ /**
138
+ * 检查器参数:隐私标签要显式放行,@brief 被关掉时跳过摘要检查。
139
+ */
140
+ export function checkerArgs(raw) {
141
+ const config = normalizeConfig(raw);
142
+ const args = [];
143
+ if (!config.brief.enabled)
144
+ args.push('--no-brief');
145
+ for (const tag of PRIVACY_TAGS) {
146
+ if (config[tag].enabled && config[tag].value)
147
+ args.push('--allow-tag', tag);
148
+ }
149
+ return args;
150
+ }
151
+ /**
152
+ * 技能快照:`get()` 把它接在技能正文末尾,让代理看到当前 profile 的标签开关。
153
+ */
154
+ export function snapshotText(raw) {
155
+ const config = normalizeConfig(raw);
156
+ const on = [];
157
+ const off = [];
158
+ for (const tag of HEADER_TAGS) {
159
+ const entry = config[tag];
160
+ const label = entry.value ? `${TAG_LABEL[tag]} ${entry.value}` : TAG_LABEL[tag];
161
+ if (entry.enabled)
162
+ on.push(label);
163
+ else
164
+ off.push(TAG_LABEL[tag]);
165
+ }
166
+ const contract = CONTRACT_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
167
+ const extra = EXTRA_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
168
+ const allowed = PRIVACY_TAGS.filter((tag) => config[tag].enabled && config[tag].value);
169
+ const head = `## 当前注释配置(profile)\n\n`;
170
+ const body = [
171
+ `- 文件头:@file 固定为文件名;开启 ${on.join('、') || '无'};关闭 ${off.join('、') || '无'}。`,
172
+ `- 函数契约:${contract.length > 0 ? contract.join('、') : '不要求'}。`,
173
+ `- 更多标签:${extra.length > 0 ? `可选 ${extra.join('、')}` : '未启用'}。`,
174
+ `- 检查器放行:${allowed.length > 0 ? allowed.join('、') : '无'}(隐私标签只有填了非空值才放行)。`,
175
+ ];
176
+ return head + body.join('\n');
177
+ }
package/lib/index.js CHANGED
@@ -21,9 +21,35 @@
21
21
  import { readdir, readFile } from 'node:fs/promises';
22
22
  import path from 'node:path';
23
23
  import { fileURLToPath } from 'node:url';
24
+ import Schema from '@deepseek-ai/schemastery';
24
25
  import { isCppWorkflowWorkspace } from './workspace.js';
26
+ import { normalizeConfig, snapshotText } from './comment-policy.js';
25
27
  export const name = 'dsh-cpp-workflow';
26
28
  export const inject = ['skills'];
29
+ // 设置页保存的标签开关;字段即时生效,改配置不必重装插件。
30
+ const HeaderTag = Schema.object({
31
+ enabled: Schema.boolean().default(false).volatile(),
32
+ value: Schema.string().default('').volatile(),
33
+ });
34
+ export const Config = Schema.object({
35
+ brief: Schema.object({
36
+ enabled: Schema.boolean().default(true).volatile(),
37
+ value: Schema.string().default('').volatile(),
38
+ }),
39
+ author: HeaderTag,
40
+ date: HeaderTag,
41
+ copyright: HeaderTag,
42
+ version: HeaderTag,
43
+ param: Schema.boolean().default(true).volatile(),
44
+ returns: Schema.boolean().default(true).volatile(),
45
+ throws: Schema.boolean().default(true).volatile(),
46
+ note: Schema.boolean().default(false).volatile(),
47
+ see: Schema.boolean().default(false).volatile(),
48
+ pre: Schema.boolean().default(false).volatile(),
49
+ post: Schema.boolean().default(false).volatile(),
50
+ tparam: Schema.boolean().default(false).volatile(),
51
+ retval: Schema.boolean().default(false).volatile(),
52
+ });
27
53
  const PROVIDER = 'dsh-cpp-workflow';
28
54
  const RANK = 550;
29
55
  const KNOWN_SKILLS = new Set(['building-cpp', 'configuring-cmake', 'developing-cpp']);
@@ -150,7 +176,7 @@ async function listSkills(options = {}) {
150
176
  candidates.sort((a, b) => a.name.localeCompare(b.name));
151
177
  return candidates;
152
178
  }
153
- async function getSkill(candidate, options = {}) {
179
+ async function getSkill(candidate, options = {}, commentConfig = undefined) {
154
180
  if (options.signal?.aborted)
155
181
  return undefined;
156
182
  if (!candidate || candidate.provider !== PROVIDER)
@@ -190,13 +216,18 @@ async function getSkill(candidate, options = {}) {
190
216
  resourceBase: { kind: 'directory', path: resolved },
191
217
  path: path.join(resolved, 'SKILL.md'),
192
218
  ...(candidate.metadata ? { metadata: candidate.metadata } : {}),
193
- content: body.trim(),
219
+ // 标签开关只跟注释技能有关,另两个技能的正文保持原样。
220
+ content: candidate.name === 'developing-cpp'
221
+ ? `${body.trim()}\n\n${snapshotText(commentConfig)}`
222
+ : body.trim(),
194
223
  };
195
224
  }
196
- export function apply(ctx) {
225
+ export function apply(ctx, config = undefined) {
226
+ // 配置决定注释标签的默认集合;快照随每次 get() 读出,改配置立即生效。
227
+ const resolved = normalizeConfig(config);
197
228
  ctx.skills.registerProvider(() => ({
198
229
  name: PROVIDER,
199
230
  list: listSkills,
200
- get: getSkill,
231
+ get: (candidate, options) => getSkill(candidate, options, resolved),
201
232
  }));
202
233
  }
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "C++ Workflow",
4
- "description": "Four source steps, three CMake steps, and three build steps for C++11–C++23 projects (C++23 by default), checked against the environment the project declares."
4
+ "description": "Write C++ sources, configure CMake, and build with Clang; /// Doxygen comments, with file-header tags you can set in Settings."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "C++ 工程流程",
4
- "description": "用源码四步、CMake 三步与构建三步推进 C++11–C++23 工程(默认 C++23),并在项目声明的环境组合里核对构建与 clang 工具链。"
4
+ "description": "写 C++ 源码、配 CMake、用 Clang 构建;文档注释统一为 /// Doxygen,文件头标签可在设置里改。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@chengzhao-dev/dsh-cpp-workflow",
3
- "version": "0.5.4",
4
- "description": "C++11–C++23 工程流程技能包:源码四步、CMake 三步与构建三步,按项目声明的环境组合校验 · C++11–C++23 workflow skills for DeepSeek Harness",
3
+ "version": "0.6.0",
4
+ "description": "写 C++ 源码、配 CMake、用 Clang 构建;文档注释统一为 /// Doxygen,文件头标签可在设置里改。 · Write C++ sources, configure CMake, and build with Clang; /// Doxygen comments, with file-header tags you can change in Settings.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "exports": {
8
8
  ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
9
10
  "./package.json": "./package.json",
10
11
  "./cordis.patch.yml": "./cordis.patch.yml"
11
12
  },
@@ -28,11 +29,17 @@
28
29
  "bundle": {
29
30
  "patch": "./cordis.patch.yml"
30
31
  },
32
+ "client": {
33
+ "platform": "web",
34
+ "inject": [
35
+ "@deepseek-ai/dsh-client-ui-slots"
36
+ ]
37
+ },
31
38
  "catalog": {
32
39
  "category": "skill",
33
40
  "summary": {
34
- "zh": "按项目声明的环境组合,推进 C++11–C++23 源码与构建流程",
35
- "en": "Advance C++11–C++23 source and build work with a project-declared environment"
41
+ "zh": "写 C++ 源码、配 CMake、用 Clang 构建;文档注释统一为 /// Doxygen,文件头标签可在设置里改。",
42
+ "en": "Write C++ sources, configure CMake, and build with Clang; /// Doxygen comments, with file-header tags you can set in Settings."
36
43
  },
37
44
  "capabilities": [
38
45
  "skills"
@@ -41,8 +48,10 @@
41
48
  },
42
49
  "icon": "./icon.svg",
43
50
  "scripts": {
44
- "build": "tsc -p tsconfig.json",
45
- "test": "npm run build && node scripts/checking-skill-links.mjs && node skills/building-cpp/scripts/checking-encoding.mjs --quiet && node scripts/checking-github-workflows.mjs && node --test tests/*.test.mjs",
51
+ "build": "tsc -p tsconfig.json && node scripts/building-client.mjs",
52
+ "test": "npm run build && node scripts/building-client.mjs --check && node scripts/checking-skill-links.mjs && node skills/building-cpp/scripts/checking-encoding.mjs --quiet && node scripts/checking-github-workflows.mjs && node --test tests/*.test.mjs",
53
+ "ci": "npm ci && npm test && npm run pack:check",
54
+ "ci:full": "npm run ci && npm run test:matrix",
46
55
  "check:encoding": "node skills/building-cpp/scripts/checking-encoding.mjs",
47
56
  "check:workflows": "node scripts/checking-github-workflows.mjs",
48
57
  "test:matrix": "bash scripts/checking-standard-matrix.sh",
@@ -77,6 +86,9 @@
77
86
  "peerDependencies": {
78
87
  "@deepseek-ai/dsh-skill": ">=0.2.0-rc.1 <0.3.0-0"
79
88
  },
89
+ "dependencies": {
90
+ "@deepseek-ai/schemastery": "^3.18.5"
91
+ },
80
92
  "devDependencies": {
81
93
  "@deepseek-ai/dsh-skill": ">=0.2.0-rc.1 <0.3.0-0",
82
94
  "@types/node": "^22.20.5",
@@ -14,11 +14,10 @@
14
14
  **好**:
15
15
 
16
16
  ```cpp
17
- //===----------------------------------------------------------------------===//
18
- // board.h - 定义井字棋棋盘、格子坐标与格子类型。
19
- //
20
- // 棋盘保存九个棋盘格子的状态,提供只读查询、落子与整盘清空。
21
- //===----------------------------------------------------------------------===//
17
+ /// @file board.h
18
+ /// @brief 定义井字棋棋盘、格子坐标与格子类型。
19
+ ///
20
+ /// 棋盘保存九个棋盘格子的状态,提供查询、落子与清空棋盘。
22
21
  ```
23
22
 
24
23
  文件头只介绍这个文件里有什么,不写作者和变更史,也不把读者指去另一个文件。
@@ -310,11 +309,10 @@ Standard: Latest
310
309
  **好**:
311
310
 
312
311
  ```cpp
313
- //===----------------------------------------------------------------------===//
314
- // board.h - 棋盘状态与规则判断
315
- //
316
- // 定义 Board 的查询、落子与整盘清空。
317
- //===----------------------------------------------------------------------===//
312
+ /// @file board.h
313
+ /// @brief 棋盘状态与规则判断。
314
+ ///
315
+ /// 定义 Board 的查询、落子与清空棋盘。
318
316
  ```
319
317
 
320
318
  变更史进提交说明,外部资料进文档,团队流程进规范文件;注释只写这个文件或这个类型自己的内容,不把读者指去别的文件。
@@ -82,7 +82,7 @@ GameResult result_ = GameResult::kInProgress;
82
82
 
83
83
  ### 实现:阶段边界与为什么
84
84
 
85
- `.cpp` / `.cc` 里,重要函数按空行分段,每一段上方写一句 `//`。句子写边界、失败后果或为什么停在这里,不把下一行条件翻译一遍;`IsWin` 这种单行函数不配注释。只写删掉会让下一次修改得到错误结果、而代码本身看不出的内容。
85
+ `.cpp` / `.cc` 里,重要函数按空行分段,每一段上方写一句 `///`。句子写边界、失败后果或为什么停在这里,不把下一行条件翻译一遍;`IsWin` 这种单行函数不配注释。只写删掉会让下一次修改得到错误结果、而代码本身看不出的内容。
86
86
 
87
87
  大型工程的共识是解释意图而不是逐步翻译:[Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html#Implementation_Comments) 要求注释解释代码在做什么,不描述代码本身;[LLVM Coding Standards](https://www.llvm.org/docs/CodingStandards.html#commenting) 要求写意图与理由。阶段注释写的就是这两类信息。
88
88
 
@@ -100,7 +100,7 @@ GameResult result_ = GameResult::kInProgress;
100
100
 
101
101
  | 层 | 行数 | 第一句 | 写什么 |
102
102
  | --- | --- | --- | --- |
103
- | 文件头 | 3–6 | 文件名 - 这个文件定义或实现什么 | 本文件由哪几部分组成 |
103
+ | 文件头 | 3–7 | `@file` 文件名 + `@brief` 一句话 | 本文件由哪几部分组成 |
104
104
  | 类型注释 | 2–14 | 名词短语:这个类型或取值是什么 | 只有类的不变量 |
105
105
  | 特殊成员 | 1–3 | 标签(默认/拷贝/移动构造函数、析构函数) | 后置条件或职责 |
106
106
  | 函数注释 | 3–10 | 正常返回什么、失败时返回或抛什么、检查什么 | 参数与失败方式 |
@@ -109,29 +109,28 @@ GameResult result_ = GameResult::kInProgress;
109
109
 
110
110
  ## 布局:放在哪一层、哪一行
111
111
 
112
- - `///` 只给声明:头文件里的类型、函数、枚举值、公开与私有数据成员。数据成员的主注释写在上一行;行尾 `///<` 只用于枚举项和 `struct` 的短字段。调用方和 Doxygen 从这里取契约。
113
- - `//` 给实现:函数体内的阶段与边界说明、局部变量、文件头横幅、配置文件的说明。
114
- - 不给局部变量和函数体里的语句写 `///`;文件头也不写成 `///`,否则编辑器会把它挂到紧随其后的第一个声明上。
115
- - 判断看一句话写的是「调用方要遵守的承诺」还是「维护者要知道的写法」:前者用 `///`,后者用 `//`。
112
+ - `///` 给声明与文件头:头文件里的类型、函数、枚举值、公开与私有数据成员,以及文件头的 `@file`/`@brief` 块。数据成员的主注释写在上一行;行尾 `///<` 只用于枚举项和 `struct` 的短字段。调用方和 Doxygen 从这里取契约。
113
+ - `///` 也给实现:函数体内的阶段与边界说明、局部变量,一律用 `///`,不混 `//`。
114
+ - `//` 只留给工具指令与别种文件:`// NOLINTNEXTLINE(...)`、`#endif // MACRO` 这类要被工具读取的标记,以及 CMake、YAML 等非 C++ 文件。
115
+ - 判断看位置:声明写契约,实现写理由,两者都用 `///`;`//` 不再表示「实现注释」。
116
116
  - 位置固定:文件头在最上方;类型注释紧贴类型声明;特殊成员(构造、析构、拷贝与移动)排在其他函数之前;函数注释紧贴函数声明;数据成员的主注释紧贴声明上一行,不写行尾补充。
117
117
  - 类数据成员(含私有)的主注释用 `///`;行尾 `///<` 只给枚举项和 `struct` 的短字段。
118
118
  - 注释和它说明的声明之间不留空行;同一个声明块内,空行只用来分组。
119
119
 
120
120
  ### 文件头
121
121
 
122
- 每个手写源文件开头写一段文件头:第一行是「文件名 - 本文件定义或实现什么」,后面只写本文件自己的组成部分或边界。删掉把读者指去另一个头文件、另一个目录或另一层的句子,也删掉「这一层不负责什么」。不写作者、版权年份、变更日志和外部资料路径。
122
+ 每个手写源文件开头写一段 doxygen 文件头:第一行 `/// @file <文件名>`,第二行 `/// @brief` 一句话说明这个文件定义或实现什么,用一行 `///` 隔开后,只写本文件自己的组成部分或边界。`@file` 的参数必须与文件名逐字相同,检查器会核对。`@brief` 默认写出;`@author`、`@date`、`@copyright`、`@version` 默认不写,只有插件配置里打开并填了非空值才写出,作者、年份和变更史不写进源码。删掉把读者指去另一个头文件、另一个目录或另一层的句子,也删掉「这一层不负责什么」。不写作者、版权年份、变更日志和外部资料路径。
123
123
 
124
124
  ```cpp
125
- //===----------------------------------------------------------------------===//
126
- // board.h - 定义井字棋棋盘、格子坐标与格子类型。
127
- //
128
- // 棋盘保存九个棋盘格子的状态,提供只读查询、落子与整盘清空。
129
- //===----------------------------------------------------------------------===//
125
+ /// @file board.h
126
+ /// @brief 定义井字棋棋盘、格子坐标与格子类型。
127
+ ///
128
+ /// 棋盘保存九个棋盘格子的状态,提供查询、落子与清空棋盘。
130
129
  ```
131
130
 
132
- - C++ 用 `//`,CMake 用 `#`,YAML/JSON 用 `#` 或 `//`。
131
+ - C++ 源文件用以上格式;CMake 用 `#`,YAML/JSON 用 `#`。
133
132
  - 超过约 200 行的文件用章节横幅分组,组名是读者要跳转的锚点;一个文件不超过 8 个横幅。
134
- - 文件头 3–6 行;第一行点文件名,后面只写本文件的范围与组成,不写实现步骤、作者和变更史。
133
+ - 文件头 3–7 行;`@file` 与 `@brief` 各占一行,后面只写本文件的范围与组成,不写实现步骤、作者和变更史。
135
134
 
136
135
  ## 排版:符号、宽度、横幅与标签
137
136
 
@@ -143,6 +142,7 @@ GameResult result_ = GameResult::kInProgress;
143
142
  - `.clang-format`、`.clang-tidy`、`.clangd`、CMake 的配置项旁边写「为什么这样设」,不写「这一项是什么」;配置注释贴在被解释的键上方。
144
143
  - 禁止:大段表格、装饰图案、表情;外部资料路径、跨仓库引用、团队流程;注释掉的代码;注释元叙述(「其余内容见某处」「两处不重复」)。
145
144
  - `///`、`@param`、横幅不进入 `.qmd`、YAML、CSS。
145
+ - C++ 源文件的说明性注释一律 `///`,实现区不与 `//` 混用;`//` 只留给文件头横幅边框与被引用的配置行。
146
146
 
147
147
  ## 润色:删复述,统一句式
148
148
 
@@ -46,11 +46,10 @@
46
46
  5. `#endif // MACRO`
47
47
 
48
48
  ```cpp
49
- //===----------------------------------------------------------------------===//
50
- // thing_table.h - 定义 ThingTable 与它用到的状态。
51
- //
52
- // 头文件放常量、枚举、结构体与类声明。
53
- //===----------------------------------------------------------------------===//
49
+ /// @file thing_table.h
50
+ /// @brief 定义 ThingTable 与它用到的状态。
51
+ ///
52
+ /// 头文件放常量、枚举、结构体与类声明。
54
53
 
55
54
  #ifndef PROJECT_PATH_FILE_H_
56
55
  #define PROJECT_PATH_FILE_H_
@@ -99,9 +98,8 @@ class ThingTable {
99
98
  4. 命名空间与定义
100
99
 
101
100
  ```cpp
102
- //===----------------------------------------------------------------------===//
103
- // thing_table.cpp - 实现 ThingTable 的添加与查询。
104
- //===----------------------------------------------------------------------===//
101
+ /// @file thing_table.cpp
102
+ /// @brief 实现 ThingTable 的添加与查询。
105
103
 
106
104
  #include "project/path/thing.h"
107
105
 
@@ -25,8 +25,12 @@ const SKIP_DIR = new Set([
25
25
  '.tmp', 'temp', 'third_party', 'third-party', 'external', 'vendor', 'deps', 'cmake-build-debug',
26
26
  'cmake-build-release', '.vs', '.idea',
27
27
  ])
28
- const FORBIDDEN_TAGS = ['@returns', '@brief', '@details', '@author', '@since', '@todo', '@date', '@file', '@copyright', '@version']
28
+ const STATIC_FORBIDDEN_TAGS = ['@returns', '@details', '@since', '@todo']
29
+ const PRIVACY_TAGS = ['@author', '@date', '@copyright', '@version']
30
+ const FORBIDDEN_TAGS = [...STATIC_FORBIDDEN_TAGS, ...PRIVACY_TAGS]
29
31
  const BANNER = /^\/\/=+-+=+\/\/$/
32
+ const DOXY_FILE = /^\/\/\/\s*@file\s+\S/
33
+ const DOXY_BRIEF = /^\/\/\/\s*@brief\s+\S/
30
34
  const MEMBER_LIST = /提供[^。]{0,40}、[^。]{0,40}(的功能|的能力|的接口)|功能[::]/
31
35
  const RATIONALE = /而不是|而非|代价.{0,4}忽略|只留一份|与其|比.{0,12}更|更(?:安全|快|清楚|直接|好写|难写)/
32
36
  const FORWARD_REF = /(?:留在|归|交给|由)\s*`?[A-Z][A-Za-z0-9_]*`?/
@@ -38,12 +42,12 @@ const DECL_KEYWORDS = /^(class|struct|union|enum|namespace|using|typedef|templat
38
42
 
39
43
  function usage(message) {
40
44
  if (message) process.stderr.write(`错误:${message}\n`)
41
- process.stderr.write('用法:node checking-comments.mjs [--root DIR] [--width N] [--banner-width N] [--no-header] [--json] [--quiet]\n')
45
+ process.stderr.write('用法:node checking-comments.mjs [--root DIR] [--width N] [--banner-width N] [--no-header] [--no-brief] [--allow-tag TAG] [--json] [--quiet]\n')
42
46
  return EXIT.usage
43
47
  }
44
48
 
45
49
  function parseArgs(argv) {
46
- const options = { root: undefined, width: 80, bannerWidth: 80, widthExplicit: false, bannerWidthExplicit: false, requireHeader: true, json: false, quiet: false }
50
+ const options = { root: undefined, width: 80, bannerWidth: 80, widthExplicit: false, bannerWidthExplicit: false, requireHeader: true, requireBrief: true, allowTags: new Set(), json: false, quiet: false }
47
51
  for (let index = 0; index < argv.length; index += 1) {
48
52
  const arg = argv[index]
49
53
  if (arg === '--root') {
@@ -59,6 +63,13 @@ function parseArgs(argv) {
59
63
  index += 1
60
64
  } else if (arg === '--no-header') {
61
65
  options.requireHeader = false
66
+ } else if (arg === '--no-brief') {
67
+ options.requireBrief = false
68
+ } else if (arg === '--allow-tag') {
69
+ const value = argv[index + 1]
70
+ if (!value) throw new Error('--allow-tag 需要标签名,例如 author')
71
+ options.allowTags.add(value.replace(/^@/, ''))
72
+ index += 1
62
73
  } else if (arg === '--json') {
63
74
  options.json = true
64
75
  } else if (arg === '--quiet') {
@@ -205,16 +216,35 @@ export function checkText(text, options = {}) {
205
216
  const inClass = classMemberLines(lines)
206
217
 
207
218
  const firstContent = lines.findIndex((line) => line.trim() !== '')
208
- if (requireHeader && firstContent !== -1 && !BANNER.test(lines[firstContent].trim())) {
209
- add(firstContent + 1, 'header-missing', '文件开头缺少文件头横幅(//===...===//)')
219
+ const firstTrimmed = firstContent === -1 ? '' : lines[firstContent].trim()
220
+ const hasDoxyHeader = DOXY_FILE.test(firstTrimmed)
221
+ let doxyHeaderEnd = -1
222
+ if (hasDoxyHeader) {
223
+ doxyHeaderEnd = firstContent
224
+ while (doxyHeaderEnd + 1 < lines.length && lines[doxyHeaderEnd + 1].trim().startsWith('///')) doxyHeaderEnd += 1
225
+ }
226
+ if (requireHeader && firstContent !== -1 && !BANNER.test(firstTrimmed) && !hasDoxyHeader) {
227
+ add(firstContent + 1, 'header-missing', '文件开头缺少文件头:写 /// @file <文件名>,下一行写 /// @brief')
210
228
  }
211
- if (firstContent !== -1 && BANNER.test(lines[firstContent].trim())) {
229
+ if (firstContent !== -1 && (BANNER.test(firstTrimmed) || hasDoxyHeader)) {
212
230
  const own = options.fileName ? options.fileName.split(/[\\/]/).pop() : ''
231
+ const bannerHeader = BANNER.test(firstTrimmed)
232
+ if (hasDoxyHeader && own) {
233
+ const declared = /@file\s+(\S+)/.exec(lines[firstContent].trim())
234
+ if (declared && declared[1] !== own) {
235
+ add(firstContent + 1, 'header-file-name', `文件头写的是 ${declared[1]},本文件是 ${own}:@file 必须等于文件名`)
236
+ }
237
+ }
238
+ if (hasDoxyHeader && options.requireBrief !== false && !DOXY_BRIEF.test((lines[firstContent + 1] ?? '').trim())) {
239
+ add(firstContent + 2, 'header-brief', '文件头第二行缺少 /// @brief 一句话说明')
240
+ }
213
241
  for (let index = firstContent + 1; index < lines.length; index += 1) {
214
242
  const trimmed = lines[index].trim()
215
- if (BANNER.test(trimmed) || !trimmed.startsWith('//')) break
243
+ if (bannerHeader ? BANNER.test(trimmed) || !trimmed.startsWith('//') : !trimmed.startsWith('///')) break
216
244
  const names = trimmed.match(/\b[A-Za-z0-9_-]+\.(?:h|hpp|cpp|cc)\b/g) ?? []
217
- for (const name of names) {
245
+ const fileMatches = trimmed.match(/@file\s+(\S+)/)
246
+ const namesToCheck = fileMatches ? names.filter((name) => name !== fileMatches[1]) : names
247
+ for (const name of namesToCheck) {
218
248
  if (name !== own) add(index + 1, 'header-cross-file', `文件头提到了别的源文件 ${name}:只介绍本文件,删掉跨文件句子`)
219
249
  }
220
250
  if (/\bapps\//.test(trimmed)) add(index + 1, 'header-cross-file', '文件头指向了另一个目录:只介绍本文件')
@@ -232,12 +262,17 @@ export function checkText(text, options = {}) {
232
262
  if (trimmed.startsWith('///') || (trimmed.startsWith('//') && !trimmed.startsWith('//!'))) {
233
263
  if (displayWidth(trimmed) > width) add(index + 1, 'comment-width', `注释行 ${displayWidth(trimmed)} 列,超过 ${width} 列`)
234
264
  for (const tag of FORBIDDEN_TAGS) {
265
+ if (options.allowTags?.has(tag.slice(1))) continue
235
266
  if (trimmed.includes(tag)) add(index + 1, 'forbidden-tag', `禁止使用 ${tag},按 references/comments.md 的标签表改写`)
236
267
  }
237
268
  }
238
269
  }
239
270
 
240
271
  for (let index = 0; index < lines.length; index += 1) {
272
+ if (doxyHeaderEnd !== -1 && index <= doxyHeaderEnd) {
273
+ index = doxyHeaderEnd
274
+ continue
275
+ }
241
276
  if (!lines[index].trim().startsWith('///')) continue
242
277
  if (lines[index].trim().startsWith('///<')) continue
243
278
  const start = index
@@ -410,6 +445,11 @@ function main(argv) {
410
445
  }
411
446
  if (options.help) return usage()
412
447
  const root = options.root ? path.resolve(options.root) : process.cwd()
448
+ try {
449
+ statSync(root)
450
+ } catch {
451
+ return usage(`根目录不存在或无法读取:${root}`)
452
+ }
413
453
  const columnLimit = readColumnLimit(root)
414
454
  if (columnLimit !== undefined) {
415
455
  if (!options.widthExplicit) options.width = columnLimit
@@ -24,7 +24,7 @@
24
24
 
25
25
  **异常声明**:构造函数、`Get` 类查询、解析函数最容易抛;在声明注释里写「当……时抛出 `std::out_of_range`」这样的具体类型,不要只写「可能抛异常」,也不要给每个参数错误都新建类型。类型选择见 `references/exceptions.md`。
26
26
 
27
- **注释**:声明注释用 `///` 写中文散文,只写调用方需要知道的;实现细节留给 `.cpp`。`@param`、`@return`、`@throws` 要么全写要么全不写,标识符和标签保持英文。写完运行 `node "<skill-dir>/scripts/checking-comments.mjs" --root <项目根>`,横幅、标签、参数完整性、成员清单与设计论证由脚本兜底。文件头、章节横幅和 TODO 的格式见 `references/comments.md`。
27
+ **注释**:声明注释用 `///` 写中文散文,只写调用方需要知道的;实现细节留给 `.cpp`。`@param`、`@return`、`@throws` 要么全写要么全不写,标识符和标签保持英文。文件头默认写 `@file` 与 `@brief` 两行,`@file` 必须与文件名相同;作者、日期、版权、版本默认不写。标签开关在插件设置页,技能正文末尾附带当前配置的快照。写完运行 `node "<skill-dir>/scripts/checking-comments.mjs" --root <项目根>`;关闭 `@brief` 时加 `--no-brief`,配置里打开了隐私标签并填了值时加 `--allow-tag <标签>`。横幅、标签、参数完整性、成员清单与设计论证由脚本兜底。文件头、章节横幅和 TODO 的格式见 `references/comments.md`。
28
28
 
29
29
  ## 正反例指针
30
30