@chengzhao-dev/dsh-cpp-workflow 0.5.5 → 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.5
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.5
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`.
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.5
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.5
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`。
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.5",
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,8 @@
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",
46
53
  "ci": "npm ci && npm test && npm run pack:check",
47
54
  "ci:full": "npm run ci && npm run test:matrix",
48
55
  "check:encoding": "node skills/building-cpp/scripts/checking-encoding.mjs",
@@ -79,6 +86,9 @@
79
86
  "peerDependencies": {
80
87
  "@deepseek-ai/dsh-skill": ">=0.2.0-rc.1 <0.3.0-0"
81
88
  },
89
+ "dependencies": {
90
+ "@deepseek-ai/schemastery": "^3.18.5"
91
+ },
82
92
  "devDependencies": {
83
93
  "@deepseek-ai/dsh-skill": ">=0.2.0-rc.1 <0.3.0-0",
84
94
  "@types/node": "^22.20.5",
@@ -17,7 +17,7 @@
17
17
  /// @file board.h
18
18
  /// @brief 定义井字棋棋盘、格子坐标与格子类型。
19
19
  ///
20
- /// 棋盘保存九个棋盘格子的状态,提供只读查询、落子与清空棋盘。
20
+ /// 棋盘保存九个棋盘格子的状态,提供查询、落子与清空棋盘。
21
21
  ```
22
22
 
23
23
  文件头只介绍这个文件里有什么,不写作者和变更史,也不把读者指去另一个文件。
@@ -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
 
@@ -119,13 +119,13 @@ GameResult result_ = GameResult::kInProgress;
119
119
 
120
120
  ### 文件头
121
121
 
122
- 每个手写源文件开头写一段 doxygen 文件头:第一行 `/// @file <文件名>`,第二行 `/// @brief` 一句话说明这个文件定义或实现什么,用一行 `///` 隔开后,只写本文件自己的组成部分或边界。删掉把读者指去另一个头文件、另一个目录或另一层的句子,也删掉「这一层不负责什么」。不写作者、版权年份、变更日志和外部资料路径。
122
+ 每个手写源文件开头写一段 doxygen 文件头:第一行 `/// @file <文件名>`,第二行 `/// @brief` 一句话说明这个文件定义或实现什么,用一行 `///` 隔开后,只写本文件自己的组成部分或边界。`@file` 的参数必须与文件名逐字相同,检查器会核对。`@brief` 默认写出;`@author`、`@date`、`@copyright`、`@version` 默认不写,只有插件配置里打开并填了非空值才写出,作者、年份和变更史不写进源码。删掉把读者指去另一个头文件、另一个目录或另一层的句子,也删掉「这一层不负责什么」。不写作者、版权年份、变更日志和外部资料路径。
123
123
 
124
124
  ```cpp
125
125
  /// @file board.h
126
126
  /// @brief 定义井字棋棋盘、格子坐标与格子类型。
127
127
  ///
128
- /// 棋盘保存九个棋盘格子的状态,提供只读查询、落子与清空棋盘。
128
+ /// 棋盘保存九个棋盘格子的状态,提供查询、落子与清空棋盘。
129
129
  ```
130
130
 
131
131
  - C++ 源文件用以上格式;CMake 用 `#`,YAML/JSON 用 `#`。
@@ -142,6 +142,7 @@ GameResult result_ = GameResult::kInProgress;
142
142
  - `.clang-format`、`.clang-tidy`、`.clangd`、CMake 的配置项旁边写「为什么这样设」,不写「这一项是什么」;配置注释贴在被解释的键上方。
143
143
  - 禁止:大段表格、装饰图案、表情;外部资料路径、跨仓库引用、团队流程;注释掉的代码;注释元叙述(「其余内容见某处」「两处不重复」)。
144
144
  - `///`、`@param`、横幅不进入 `.qmd`、YAML、CSS。
145
+ - C++ 源文件的说明性注释一律 `///`,实现区不与 `//` 混用;`//` 只留给文件头横幅边框与被引用的配置行。
145
146
 
146
147
  ## 润色:删复述,统一句式
147
148
 
@@ -25,7 +25,9 @@ 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', '@details', '@author', '@since', '@todo', '@date', '@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 = /^\/\/=+-+=+\/\/$/
30
32
  const DOXY_FILE = /^\/\/\/\s*@file\s+\S/
31
33
  const DOXY_BRIEF = /^\/\/\/\s*@brief\s+\S/
@@ -40,12 +42,12 @@ const DECL_KEYWORDS = /^(class|struct|union|enum|namespace|using|typedef|templat
40
42
 
41
43
  function usage(message) {
42
44
  if (message) process.stderr.write(`错误:${message}\n`)
43
- 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')
44
46
  return EXIT.usage
45
47
  }
46
48
 
47
49
  function parseArgs(argv) {
48
- 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 }
49
51
  for (let index = 0; index < argv.length; index += 1) {
50
52
  const arg = argv[index]
51
53
  if (arg === '--root') {
@@ -61,6 +63,13 @@ function parseArgs(argv) {
61
63
  index += 1
62
64
  } else if (arg === '--no-header') {
63
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
64
73
  } else if (arg === '--json') {
65
74
  options.json = true
66
75
  } else if (arg === '--quiet') {
@@ -220,7 +229,13 @@ export function checkText(text, options = {}) {
220
229
  if (firstContent !== -1 && (BANNER.test(firstTrimmed) || hasDoxyHeader)) {
221
230
  const own = options.fileName ? options.fileName.split(/[\\/]/).pop() : ''
222
231
  const bannerHeader = BANNER.test(firstTrimmed)
223
- if (hasDoxyHeader && !DOXY_BRIEF.test((lines[firstContent + 1] ?? '').trim())) {
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())) {
224
239
  add(firstContent + 2, 'header-brief', '文件头第二行缺少 /// @brief 一句话说明')
225
240
  }
226
241
  for (let index = firstContent + 1; index < lines.length; index += 1) {
@@ -247,6 +262,7 @@ export function checkText(text, options = {}) {
247
262
  if (trimmed.startsWith('///') || (trimmed.startsWith('//') && !trimmed.startsWith('//!'))) {
248
263
  if (displayWidth(trimmed) > width) add(index + 1, 'comment-width', `注释行 ${displayWidth(trimmed)} 列,超过 ${width} 列`)
249
264
  for (const tag of FORBIDDEN_TAGS) {
265
+ if (options.allowTags?.has(tag.slice(1))) continue
250
266
  if (trimmed.includes(tag)) add(index + 1, 'forbidden-tag', `禁止使用 ${tag},按 references/comments.md 的标签表改写`)
251
267
  }
252
268
  }
@@ -429,6 +445,11 @@ function main(argv) {
429
445
  }
430
446
  if (options.help) return usage()
431
447
  const root = options.root ? path.resolve(options.root) : process.cwd()
448
+ try {
449
+ statSync(root)
450
+ } catch {
451
+ return usage(`根目录不存在或无法读取:${root}`)
452
+ }
432
453
  const columnLimit = readColumnLimit(root)
433
454
  if (columnLimit !== undefined) {
434
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