@chengzhao-dev/dsh-cpp-workflow 0.6.0 → 0.6.2

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,13 +19,13 @@ 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.6.0
22
+ dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.2
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.6.0
28
+ dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.2
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.
package/README.md CHANGED
@@ -19,13 +19,13 @@
19
19
  npm 安装钉死版本:
20
20
 
21
21
  ```bash
22
- dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.0
22
+ dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.2
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.6.0
28
+ dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.2
29
29
  ```
30
30
 
31
31
  桌面应用独占 `desktop` profile。命令行 `add` 被拒绝时,在应用设置里添加插件,地址填同一个 npm 版本。Git 标签安装使用仓库内已构建的 `lib/`,不在目标机器上重新构建;包内也不含需要授权执行的安装脚本。
package/lib/client.js CHANGED
@@ -57,6 +57,10 @@ const TAG_LABEL = {
57
57
  tparam: '@tparam',
58
58
  retval: '@retval',
59
59
  };
60
+ /** 预览里的示例摘要:用户没填 @brief 时用它,避免预览出现空标签。 */
61
+ const SAMPLE_BRIEF = '<一句话说明本文件>';
62
+ /** 预览里的示例文件名:只是占位符,不对应任何工程的真实文件。 */
63
+ const SAMPLE_FILE = '<文件名>';
60
64
  /** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
61
65
  const DEFAULT_CONFIG = {
62
66
  brief: { enabled: true, value: '' },
@@ -103,7 +107,7 @@ function normalizeConfig(raw) {
103
107
  */
104
108
  function renderFileHeader(raw, fileName) {
105
109
  const config = normalizeConfig(raw);
106
- const name = text(fileName) || 'board.h';
110
+ const name = text(fileName) || SAMPLE_FILE;
107
111
  const lines = [`/// @file ${name}`];
108
112
  if (config.brief.enabled) {
109
113
  lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
@@ -119,15 +123,19 @@ function renderFileHeader(raw, fileName) {
119
123
  */
120
124
  function previewParts(raw, sampleName) {
121
125
  const config = normalizeConfig(raw);
122
- const header = renderFileHeader(config, sampleName || 'board.h');
126
+ const header = renderFileHeader(config, sampleName || SAMPLE_FILE);
127
+ // 预览是示例:@brief 开着但用户没填句子时,用示例文件自己的摘要顶替。
128
+ const briefIndex = header.indexOf('/// @brief');
129
+ if (briefIndex !== -1)
130
+ header[briefIndex] = `/// @brief ${SAMPLE_BRIEF}`;
123
131
  const contract = [];
124
132
  if (config.param)
125
- contract.push('/// @param position 目标行列坐标。');
133
+ contract.push('/// @param row 目标行号。');
126
134
  if (config.returns)
127
135
  contract.push('/// @return 放置成功时为 true。');
128
136
  if (config.throws)
129
- contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
130
- const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
137
+ contract.push('/// @throws std::out_of_range 行号越界时抛出。');
138
+ const declaration = '[[nodiscard]] bool Place(int row, int column);';
131
139
  return { header, contract, declaration };
132
140
  }
133
141
  /**
@@ -135,7 +143,7 @@ function previewParts(raw, sampleName) {
135
143
  */
136
144
  function previewText(raw, sampleName) {
137
145
  const { header, contract, declaration } = previewParts(raw, sampleName);
138
- const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
146
+ const lines = [...header, '///', '/// 放置成功时返回 true;行号越界或目标格子已占时返回 false。'];
139
147
  if (contract.length > 0)
140
148
  lines.push('///', ...contract);
141
149
  lines.push(declaration);
@@ -187,28 +195,36 @@ function snapshotText(raw) {
187
195
  // 客户端半侧:设置导航里的「C++ 工程流程」页与插件页的配置卡片共用同一个组件。
188
196
  // 标签策略在本文件上方内联(由 scripts/building-client.mjs 从 src/comment-policy.js
189
197
  // 拼进来),所以预览、默认值与服务端快照永远同源;这里只做表单与排版。
198
+ //
199
+ // 版式参照 VS Code 设置页:左侧标签与说明、右侧控件,行间细分隔线;
200
+ // 修改过默认值的行在标签前加一个圆点,底部固定一条保存栏。
190
201
  const React = require('react')
191
202
  const h = React.createElement
192
203
 
193
204
  const PACKAGE_NAME = '@chengzhao-dev/dsh-cpp-workflow'
194
- const SAMPLE_FILE = 'board.h'
195
205
 
196
206
  const STRINGS = {
197
207
  zh: {
198
208
  nav: 'C++ 工程流程',
199
209
  title: '文档注释',
200
- intro: '文件头与函数契约默认写出哪些 Doxygen 标签。配置只影响代理写注释的默认值,不改已有源码。',
210
+ intro: '设置文件头与函数声明注释里默认写出的 Doxygen 标签;改动影响之后写出的注释,已有源码保持原样。',
201
211
  header: '文件头',
212
+ headerHint: '每个手写源文件开头的一段 /// 注释。',
202
213
  contract: '函数契约',
214
+ contractHint: '函数声明注释里要求的参数与失败信息。',
203
215
  extra: '更多标签',
204
- extraHint: '默认关闭;需要时在单个函数上补写。',
216
+ extraHint: '需要时在单个函数上补写。',
205
217
  file: '@file',
206
- fileHint: '固定为当前文件名,不参与配置。',
207
- emptyValue: '留空则由每个文件自己写',
208
- on: '写出',
209
- off: '不写',
218
+ fileHint: '取自当前文件名。',
219
+ briefHint: '一句话说明这个文件定义或实现什么;留空则由文件自述。',
220
+ authorHint: '作者署名;留空不写。',
221
+ dateHint: '创建或修改日期;留空不写。',
222
+ copyrightHint: '版权声明;留空不写。',
223
+ versionHint: '文件版本号;留空不写。',
224
+ emptyValue: '留空不写',
225
+ briefEmpty: '留空则由文件自述',
210
226
  preview: '预览',
211
- previewHint: '示例文件名 board.h;预览随开关即时更新。',
227
+ previewHint: '按当前开关生成;文件名以占位符表示。',
212
228
  save: '保存',
213
229
  reset: '重置',
214
230
  saved: '已保存,下一次读取技能时生效。',
@@ -216,24 +232,31 @@ const STRINGS = {
216
232
  unsaved: '有未保存的改动。',
217
233
  loading: '正在读取配置…',
218
234
  unreadable: '当前账户不能修改这份配置。',
219
- readOnly: '此处只显示出厂值。要修改配置,请到「插件」页打开 dsh-cpp-workflow 的配置卡片。',
220
- noForm: '这个入口没有可写表单,配置在插件页修改。',
235
+ readOnly: '此处显示出厂值;修改在「插件」页的配置卡片里进行。',
236
+ openSettings: '打开插件配置',
237
+ modified: '与出厂值不同',
221
238
  },
222
239
  en: {
223
240
  nav: 'C++ Workflow',
224
241
  title: 'Documentation comments',
225
- intro: 'Which Doxygen tags the agent writes by default. This changes defaults only; existing sources are untouched.',
242
+ intro: 'Choose the Doxygen tags written by default in file headers and declaration comments. Changes affect new comments; existing sources stay as they are.',
226
243
  header: 'File header',
244
+ headerHint: 'The /// block at the top of every hand-written source.',
227
245
  contract: 'Function contract',
246
+ contractHint: 'Parameter and failure tags required in declaration comments.',
228
247
  extra: 'More tags',
229
- extraHint: 'Off by default; add them per function when needed.',
248
+ extraHint: 'Add them per function when needed.',
230
249
  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',
250
+ fileHint: 'Taken from the current file name.',
251
+ briefHint: 'One sentence about what this file defines; leave empty and the file writes its own.',
252
+ authorHint: 'Author credit; left empty, omitted.',
253
+ dateHint: 'Creation or change date; left empty, omitted.',
254
+ copyrightHint: 'Copyright notice; left empty, omitted.',
255
+ versionHint: 'File version; left empty, omitted.',
256
+ emptyValue: 'Empty means omit',
257
+ briefEmpty: 'Empty: the file writes its own',
235
258
  preview: 'Preview',
236
- previewHint: 'Sample name board.h; the preview follows the switches.',
259
+ previewHint: 'Generated from the switches; the file name is a placeholder.',
237
260
  save: 'Save',
238
261
  reset: 'Reset',
239
262
  saved: 'Saved; the next skill read uses it.',
@@ -241,8 +264,9 @@ const STRINGS = {
241
264
  unsaved: 'Unsaved changes.',
242
265
  loading: 'Reading configuration…',
243
266
  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.',
267
+ readOnly: 'Factory values are shown here; edit them on the Plugins page.',
268
+ openSettings: 'Open plugin settings',
269
+ modified: 'Differs from the factory value',
246
270
  },
247
271
  }
248
272
 
@@ -256,92 +280,188 @@ function t() {
256
280
  }
257
281
 
258
282
  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))',
283
+ label: 'var(--dsw-alias-label-primary, inherit)',
284
+ muted: 'var(--dsw-alias-label-secondary, #6b7280)',
285
+ line: 'var(--dsw-alias-border-l2, rgba(127, 127, 127, 0.24))',
286
+ surface: 'var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, 0.06))',
287
+ field: 'var(--dsw-alias-bg-layer-1, rgba(127, 127, 127, 0.04))',
288
+ brand: 'var(--dsw-alias-brand-primary, #4f6bed)',
262
289
  mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
263
290
  }
264
291
 
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
- }
292
+ const descriptionStyle = () => ({ margin: '2px 0 0', fontSize: 12, lineHeight: 1.55, color: palette.muted })
293
+ const sectionTitleStyle = () => ({ margin: '0 0 2px', fontSize: 13, fontWeight: 600, color: palette.label })
294
+
295
+ function Section(props) {
296
+ return h(
297
+ 'section',
298
+ {
299
+ style: {
300
+ marginTop: props.first ? 16 : 22,
301
+ paddingTop: props.first ? 0 : 14,
302
+ borderTop: props.first ? 'none' : `1px solid ${palette.line}`,
303
+ },
304
+ },
305
+ h('h4', { style: sectionTitleStyle() }, props.title),
306
+ props.description ? h('p', { style: descriptionStyle() }, props.description) : null,
307
+ h('div', { style: { marginTop: 6 } }, props.children),
308
+ )
274
309
  }
275
310
 
276
- function hintStyle() {
277
- return { margin: '2px 0 0', fontSize: 12, lineHeight: 1.5, color: palette.muted }
311
+ function Row(props) {
312
+ return h(
313
+ 'div',
314
+ {
315
+ style: {
316
+ display: 'grid',
317
+ gridTemplateColumns: 'minmax(0, 1fr) minmax(160px, 240px)',
318
+ columnGap: 24,
319
+ alignItems: 'start',
320
+ padding: '10px 0',
321
+ borderBottom: `1px solid ${palette.line}`,
322
+ },
323
+ },
324
+ h(
325
+ 'div',
326
+ { style: { minWidth: 0 } },
327
+ h(
328
+ 'div',
329
+ { style: { display: 'flex', alignItems: 'center', gap: 6, minHeight: 20 } },
330
+ props.modified
331
+ ? h('span', {
332
+ key: 'dot',
333
+ title: props.modifiedLabel,
334
+ 'aria-label': props.modifiedLabel,
335
+ style: { width: 6, height: 6, borderRadius: '50%', background: palette.brand, flex: 'none' },
336
+ })
337
+ : null,
338
+ h('span', { style: { fontSize: 13, lineHeight: '20px', color: palette.label, fontFamily: props.mono ? palette.mono : 'inherit' } }, props.label),
339
+ ),
340
+ props.description ? h('p', { style: descriptionStyle() }, props.description) : null,
341
+ ),
342
+ h(
343
+ 'div',
344
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, minHeight: 20 } },
345
+ props.control,
346
+ ),
347
+ )
278
348
  }
279
349
 
280
- function Switch(props) {
350
+ function Checkbox(props) {
281
351
  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),
352
+ 'button',
353
+ {
354
+ type: 'button',
355
+ role: 'checkbox',
356
+ 'aria-checked': Boolean(props.checked),
357
+ 'aria-label': props.label,
287
358
  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),
359
+ onClick: () => props.onChange(!props.checked),
360
+ style: {
361
+ flex: 'none',
362
+ width: 16,
363
+ height: 16,
364
+ padding: 0,
365
+ borderRadius: 4,
366
+ border: `1px solid ${props.checked ? palette.brand : palette.line}`,
367
+ background: props.checked ? palette.brand : 'transparent',
368
+ display: 'inline-flex',
369
+ alignItems: 'center',
370
+ justifyContent: 'center',
371
+ cursor: props.disabled ? 'default' : 'pointer',
372
+ opacity: props.disabled ? 0.55 : 1,
373
+ transition: 'background-color 0.12s ease, border-color 0.12s ease',
374
+ },
375
+ },
376
+ h(
377
+ 'svg',
378
+ { width: 11, height: 11, viewBox: '0 0 12 12', 'aria-hidden': 'true' },
379
+ h('path', {
380
+ d: 'M2.6 6.3 5 8.7l4.4-5.2',
381
+ fill: 'none',
382
+ stroke: '#fff',
383
+ strokeWidth: 1.7,
384
+ strokeLinecap: 'round',
385
+ strokeLinejoin: 'round',
386
+ opacity: props.checked ? 1 : 0,
387
+ }),
388
+ ),
292
389
  )
293
390
  }
294
391
 
295
- function HeaderTagRow(props) {
392
+ function ValueRow(props) {
296
393
  const s = props.strings
297
394
  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, {
395
+ return h(Row, {
396
+ label: TAG_LABEL[props.tag],
397
+ mono: true,
398
+ description: props.hint,
399
+ modified: props.modified,
400
+ modifiedLabel: s.modified,
401
+ control: [
402
+ h(Checkbox, {
403
+ key: 'check',
303
404
  label: TAG_LABEL[props.tag],
304
405
  checked: entry.enabled,
406
+ disabled: props.disabled,
305
407
  onChange: (enabled) => props.onChange({ ...entry, enabled }),
306
408
  }),
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
- },
409
+ h('input', {
410
+ key: 'input',
411
+ type: 'text',
412
+ value: entry.value,
413
+ placeholder: props.tag === 'brief' ? s.briefEmpty : s.emptyValue,
414
+ disabled: props.disabled || !entry.enabled,
415
+ 'aria-label': TAG_LABEL[props.tag],
416
+ onChange: (event) => props.onChange({ ...entry, value: event.target.value }),
417
+ style: {
418
+ width: '100%',
419
+ minWidth: 0,
420
+ fontSize: 13,
421
+ lineHeight: '18px',
422
+ padding: '3px 7px',
423
+ borderRadius: 4,
424
+ border: `1px solid ${palette.line}`,
425
+ background: entry.enabled && !props.disabled ? palette.field : 'transparent',
426
+ color: entry.enabled ? 'inherit' : palette.muted,
427
+ fontFamily: palette.mono,
428
+ boxSizing: 'border-box',
429
+ },
430
+ }),
431
+ ],
432
+ })
433
+ }
434
+
435
+ function FlagRow(props) {
436
+ return h(Row, {
437
+ label: TAG_LABEL[props.tag],
438
+ mono: true,
439
+ description: props.hint,
440
+ modified: props.modified,
441
+ modifiedLabel: props.strings.modified,
442
+ control: h(Checkbox, {
443
+ label: TAG_LABEL[props.tag],
444
+ checked: Boolean(props.checked),
445
+ disabled: props.disabled,
446
+ onChange: (value) => props.onChange(value),
326
447
  }),
327
- )
448
+ })
328
449
  }
329
450
 
330
451
  function Preview(props) {
331
452
  const s = props.strings
332
- const text = previewText(props.config, SAMPLE_FILE)
333
453
  return h(
334
454
  '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),
455
+ { 'aria-label': s.preview, style: { marginTop: 22, paddingTop: 14, borderTop: `1px solid ${palette.line}` } },
456
+ h('h4', { style: sectionTitleStyle() }, s.preview),
457
+ h('p', { style: descriptionStyle() }, s.previewHint),
338
458
  h(
339
459
  'pre',
340
460
  {
341
461
  style: {
342
462
  margin: '8px 0 0',
343
463
  padding: '10px 12px',
344
- borderRadius: 8,
464
+ borderRadius: 6,
345
465
  background: palette.surface,
346
466
  border: `1px solid ${palette.line}`,
347
467
  overflowX: 'auto',
@@ -349,9 +469,10 @@ function Preview(props) {
349
469
  lineHeight: 1.65,
350
470
  fontFamily: palette.mono,
351
471
  whiteSpace: 'pre',
472
+ color: palette.label,
352
473
  },
353
474
  },
354
- text,
475
+ previewText(props.config),
355
476
  ),
356
477
  )
357
478
  }
@@ -366,6 +487,10 @@ function diffOperations(draft, base) {
366
487
  return operations
367
488
  }
368
489
 
490
+ function isModified(key, draft) {
491
+ return JSON.stringify(draft[key]) !== JSON.stringify(DEFAULT_CONFIG[key])
492
+ }
493
+
369
494
  function ConfigPage(props) {
370
495
  const s = t()
371
496
  const form = props.form
@@ -420,94 +545,121 @@ function ConfigPage(props) {
420
545
  }
421
546
 
422
547
  const editable = Boolean(form)
423
- const editableReady = editable && ready
424
- const headerTags = ['brief', 'author', 'date', 'copyright', 'version']
548
+ const headerHints = {
549
+ brief: s.briefHint,
550
+ author: s.authorHint,
551
+ date: s.dateHint,
552
+ copyright: s.copyrightHint,
553
+ version: s.versionHint,
554
+ }
425
555
 
426
556
  return h(
427
557
  'div',
428
- { style: { display: 'flex', flexDirection: 'column', gap: 4, padding: '4px 0 12px', maxWidth: 720 } },
558
+ { style: { display: 'flex', flexDirection: 'column', padding: '2px 0 8px', maxWidth: 760, color: palette.label } },
429
559
  h('h3', { style: { margin: '0 0 2px', fontSize: 15, fontWeight: 600 } }, s.title),
430
- h('p', { style: hintStyle() }, s.intro),
560
+ h('p', { style: descriptionStyle() }, s.intro),
431
561
  !editable
432
- ? h('p', { role: 'status', style: { ...hintStyle(), color: 'inherit', marginTop: 8 } }, s.readOnly)
562
+ ? h(
563
+ 'div',
564
+ {
565
+ role: 'status',
566
+ style: {
567
+ display: 'flex',
568
+ alignItems: 'center',
569
+ gap: 10,
570
+ flexWrap: 'wrap',
571
+ marginTop: 10,
572
+ padding: '8px 10px',
573
+ borderRadius: 6,
574
+ background: palette.surface,
575
+ border: `1px solid ${palette.line}`,
576
+ },
577
+ },
578
+ h('p', { style: { ...descriptionStyle(), margin: 0, flex: '1 1 200px' } }, s.readOnly),
579
+ props.openPluginPage
580
+ ? h('button', { type: 'button', onClick: props.openPluginPage, style: secondaryButton(true) }, s.openSettings)
581
+ : null,
582
+ )
433
583
  : null,
434
584
  editable && !ready && status !== 'missing'
435
- ? h('p', { role: 'status', style: hintStyle() }, status === 'loading' ? s.loading : s.unreadable)
585
+ ? h('p', { role: 'status', style: descriptionStyle() }, status === 'loading' ? s.loading : s.unreadable)
436
586
  : null,
437
587
  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, {
588
+ Section,
589
+ { title: s.header, description: s.headerHint, first: true },
590
+ h(Row, {
591
+ label: s.file,
592
+ mono: true,
593
+ description: s.fileHint,
594
+ control: null,
595
+ }),
596
+ ...HEADER_TAGS.map((tag) =>
597
+ h(ValueRow, {
449
598
  key: tag,
450
599
  tag,
600
+ hint: headerHints[tag],
451
601
  strings: s,
602
+ disabled: !editable,
603
+ modified: editable && isModified(tag, draft),
452
604
  entry: draft[tag],
453
605
  onChange: (entry) => update({ [tag]: entry }),
454
606
  }),
455
607
  ),
456
608
  ),
457
609
  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
- ),
610
+ Section,
611
+ { title: s.contract, description: s.contractHint },
612
+ ...CONTRACT_TAGS.map((tag) =>
613
+ h(FlagRow, {
614
+ key: tag,
615
+ tag,
616
+ hint: TAG_TEXT[tag],
617
+ strings: s,
618
+ disabled: !editable,
619
+ modified: editable && isModified(tag, draft),
620
+ checked: draft[tag],
621
+ onChange: (value) => update({ [tag]: value }),
622
+ }),
476
623
  ),
477
624
  ),
478
625
  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
- ),
626
+ Section,
627
+ { title: s.extra, description: s.extraHint },
628
+ ...EXTRA_TAGS.map((tag) =>
629
+ h(FlagRow, {
630
+ key: tag,
631
+ tag,
632
+ hint: TAG_TEXT[tag],
633
+ strings: s,
634
+ disabled: !editable,
635
+ modified: editable && isModified(tag, draft),
636
+ checked: draft[tag],
637
+ onChange: (value) => update({ [tag]: value }),
638
+ }),
497
639
  ),
498
640
  ),
499
641
  h(Preview, { config: draft, strings: s }),
500
642
  editable
501
643
  ? h(
502
644
  'div',
503
- { style: { display: 'flex', alignItems: 'center', gap: 10, marginTop: 16, flexWrap: 'wrap' } },
645
+ {
646
+ style: {
647
+ display: 'flex',
648
+ alignItems: 'center',
649
+ gap: 10,
650
+ marginTop: 20,
651
+ paddingTop: 12,
652
+ borderTop: `1px solid ${palette.line}`,
653
+ flexWrap: 'wrap',
654
+ },
655
+ },
504
656
  h(
505
657
  'button',
506
658
  {
507
659
  type: 'button',
508
660
  onClick: save,
509
- disabled: busy || !editableReady || !dirty,
510
- style: { fontSize: 13, padding: '6px 14px', borderRadius: 6, cursor: 'pointer' },
661
+ disabled: busy || !ready || !dirty,
662
+ style: primaryButton(dirty && ready && !busy),
511
663
  },
512
664
  s.save,
513
665
  ),
@@ -517,22 +669,55 @@ function ConfigPage(props) {
517
669
  type: 'button',
518
670
  onClick: reset,
519
671
  disabled: busy || !dirty,
520
- style: { fontSize: 13, padding: '6px 14px', borderRadius: 6, cursor: 'pointer' },
672
+ style: secondaryButton(dirty && !busy),
521
673
  },
522
674
  s.reset,
523
675
  ),
524
- h('p', { role: 'status', style: { ...hintStyle(), margin: 0, flex: '1 1 220px' } }, message || (dirty ? s.unsaved : '')),
676
+ h('p', { role: 'status', style: { ...descriptionStyle(), margin: 0, flex: '1 1 200px' } }, message || (dirty ? s.unsaved : '')),
525
677
  )
526
678
  : null,
527
679
  )
528
680
  }
529
681
 
530
- function page(ownerProps, titled) {
682
+ function primaryButton(enabled) {
683
+ return {
684
+ fontSize: 13,
685
+ lineHeight: '20px',
686
+ padding: '4px 12px',
687
+ borderRadius: 4,
688
+ border: '1px solid transparent',
689
+ background: enabled ? palette.brand : 'var(--dsw-alias-bg-layer-3, rgba(127, 127, 127, 0.2))',
690
+ color: enabled ? '#fff' : palette.muted,
691
+ cursor: enabled ? 'pointer' : 'default',
692
+ }
693
+ }
694
+
695
+ function secondaryButton(enabled) {
696
+ return {
697
+ fontSize: 13,
698
+ lineHeight: '20px',
699
+ padding: '4px 12px',
700
+ borderRadius: 4,
701
+ border: `1px solid ${palette.line}`,
702
+ background: 'transparent',
703
+ color: enabled ? palette.label : palette.muted,
704
+ cursor: enabled ? 'pointer' : 'default',
705
+ }
706
+ }
707
+
708
+ function page(ownerProps, openPluginPage) {
531
709
  const props = ownerProps || {}
532
- return h(ConfigPage, { ...props, titled })
710
+ return h(ConfigPage, { ...props, openPluginPage })
533
711
  }
534
712
 
535
713
  function apply(ctx) {
714
+ let openPluginPage
715
+ try {
716
+ const navigation = typeof ctx.get === 'function' ? ctx.get('pluginNavigation') : undefined
717
+ if (navigation && typeof navigation.openBundle === 'function') {
718
+ openPluginPage = () => navigation.openBundle(PACKAGE_NAME)
719
+ }
720
+ } catch {}
536
721
  ctx.slots.inject('settings.section', () =>
537
722
  ctx.slots.register(
538
723
  {
@@ -541,7 +726,7 @@ function apply(ctx) {
541
726
  order: 60,
542
727
  label: () => t().nav,
543
728
  },
544
- (ownerProps) => page(ownerProps, true),
729
+ (ownerProps) => page(ownerProps, openPluginPage),
545
730
  ),
546
731
  )
547
732
  ctx.slots.inject('plugins.bundle.config', () =>
@@ -551,7 +736,7 @@ function apply(ctx) {
551
736
  key: PACKAGE_NAME,
552
737
  label: () => t().nav,
553
738
  },
554
- (ownerProps) => (ownerProps && ownerProps.view === 'summary' ? null : page(ownerProps, false)),
739
+ (ownerProps) => (ownerProps && ownerProps.view === 'summary' ? null : page(ownerProps, undefined)),
555
740
  ),
556
741
  )
557
742
  }
@@ -50,6 +50,10 @@ export const TAG_LABEL = {
50
50
  tparam: '@tparam',
51
51
  retval: '@retval',
52
52
  };
53
+ /** 预览里的示例摘要:用户没填 @brief 时用它,避免预览出现空标签。 */
54
+ export const SAMPLE_BRIEF = '<一句话说明本文件>';
55
+ /** 预览里的示例文件名:只是占位符,不对应任何工程的真实文件。 */
56
+ export const SAMPLE_FILE = '<文件名>';
53
57
  /** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
54
58
  export const DEFAULT_CONFIG = {
55
59
  brief: { enabled: true, value: '' },
@@ -96,7 +100,7 @@ export function normalizeConfig(raw) {
96
100
  */
97
101
  export function renderFileHeader(raw, fileName) {
98
102
  const config = normalizeConfig(raw);
99
- const name = text(fileName) || 'board.h';
103
+ const name = text(fileName) || SAMPLE_FILE;
100
104
  const lines = [`/// @file ${name}`];
101
105
  if (config.brief.enabled) {
102
106
  lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
@@ -112,15 +116,19 @@ export function renderFileHeader(raw, fileName) {
112
116
  */
113
117
  export function previewParts(raw, sampleName) {
114
118
  const config = normalizeConfig(raw);
115
- const header = renderFileHeader(config, sampleName || 'board.h');
119
+ const header = renderFileHeader(config, sampleName || SAMPLE_FILE);
120
+ // 预览是示例:@brief 开着但用户没填句子时,用示例文件自己的摘要顶替。
121
+ const briefIndex = header.indexOf('/// @brief');
122
+ if (briefIndex !== -1)
123
+ header[briefIndex] = `/// @brief ${SAMPLE_BRIEF}`;
116
124
  const contract = [];
117
125
  if (config.param)
118
- contract.push('/// @param position 目标行列坐标。');
126
+ contract.push('/// @param row 目标行号。');
119
127
  if (config.returns)
120
128
  contract.push('/// @return 放置成功时为 true。');
121
129
  if (config.throws)
122
- contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
123
- const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
130
+ contract.push('/// @throws std::out_of_range 行号越界时抛出。');
131
+ const declaration = '[[nodiscard]] bool Place(int row, int column);';
124
132
  return { header, contract, declaration };
125
133
  }
126
134
  /**
@@ -128,7 +136,7 @@ export function previewParts(raw, sampleName) {
128
136
  */
129
137
  export function previewText(raw, sampleName) {
130
138
  const { header, contract, declaration } = previewParts(raw, sampleName);
131
- const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
139
+ const lines = [...header, '///', '/// 放置成功时返回 true;行号越界或目标格子已占时返回 false。'];
132
140
  if (contract.length > 0)
133
141
  lines.push('///', ...contract);
134
142
  lines.push(declaration);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chengzhao-dev/dsh-cpp-workflow",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
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",
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  ## 适用场景
11
11
 
12
- 管 `environment.json`、`running-build.sh`、`.clang-format`、`.clang-tidy`、`.clangd`、`.vscode/`、构建命令、编译数据库、CTest 和运行。`CMakeLists.txt` 与 `cmake/` 下的 target、属性、标准和模块由 `configuring-cmake` 负责;本技能只读它们来配置和检查,不在这一步改 target 图。函数体、类型、命名、异常和源码注释只开 `developing-cpp`。
12
+ 本技能处理环境声明、构建入口与工具配置:`environment.json`、`running-build.sh`、`.clang-format`、`.clang-tidy`、`.clangd`、`.vscode/`、构建命令、编译数据库、CTest 和运行。`CMakeLists.txt` 与 `cmake/` 下的 target、属性、标准和模块由 `configuring-cmake` 负责;本技能只读它们来配置和检查,不在这一步改 target 图。函数体、类型、命名、异常和源码注释只开 `developing-cpp`。
13
13
 
14
14
  一次改动同时碰到源码和构建文件时,先打开 `developing-cpp` 定行为、边界与实现;需要新增或移动源文件、改库与程序的边界、补测试 target 时,转到 `configuring-cmake` 把文件写进 CMake;最后回到本技能做定工具配置与通过检查。只改函数体、命名、异常或注释且入口命令可运行时,定实现后只开本技能的「通过检查」。
15
15
 
@@ -26,7 +26,7 @@ metadata:
26
26
  | 增量修正 | 用户点了一条工具配置、一条诊断或一个入口脚本错误 | 先判断属于定构建还是定工具配置,只改那一处,不动无关 target 和配置 |
27
27
  | 整段重构 | 新工程、新阶段、换环境组合,或第一次接构建 | 同样三步;先确认 target 与标准已由 configuring-cmake 写好,再改工具文件,最后跑完整检查 |
28
28
 
29
- 共同不变量:不发明项目没有声明的环境轴、命令和 target;只改格式或配置时不改可观察行为;不覆盖已有源码、`CMakeLists.txt` 和 `.clang-*`;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。后一步发现前一步没做完,退回那一步。拿不准先用增量修正;发现环境轴、编译器、构建目录或 target 契约需要改,退回整段,并转 `configuring-cmake`。
29
+ 两种走法共用这些约束:不发明项目没有声明的环境轴、命令和 target;只改格式或配置时不改可观察行为;不覆盖已有源码、`CMakeLists.txt` 和 `.clang-*`;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。后一步发现前一步没做完,退回那一步。拿不准先用增量修正;发现环境轴、编译器、构建目录或 target 契约需要改,退回整段,并转 `configuring-cmake`。
30
30
 
31
31
  ## 步骤
32
32
 
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  ## 适用场景
11
11
 
12
- 管 `CMakeLists.txt`、`cmake/` 下的模块、`target_*` 命令、目标属性、include 与链接边界、输出目录、共享库运行时路径、`FetchContent`、preset 与安装导出。函数体、类型、命名、异常和源码注释只开 `developing-cpp`;配置完成后要跑构建、格式化、静态检查或入口脚本时开 `building-cpp`。
12
+ 本技能处理 CMake 文件与 target 契约:`CMakeLists.txt`、`cmake/` 模块、`target_*` 命令、目标属性、include 与链接边界、输出目录、共享库运行时路径、`FetchContent`、preset 与安装导出。函数体、类型、命名、异常和源码注释只开 `developing-cpp`;配置完成后要跑构建、格式化、静态检查或入口脚本时开 `building-cpp`。
13
13
 
14
14
  一次改动同时碰到源码和 CMake 时先开 `developing-cpp` 定行为与边界,再用本技能把源文件、库和程序写进 target,最后到 `building-cpp` 跑检查。只改一个 target 的 include、链接或标准时走增量修正;新工程、新组件、换语言标准或抽公共函数时走整段重构。
15
15
 
@@ -22,7 +22,7 @@ metadata:
22
22
  | 增量修正 | 一个 target 的 include、链接、标准、输出目录或一条属性不对 | 先读现有 `CMakeLists.txt`,只改这一处,不动无关 target 和全局设置 |
23
23
  | 整段重构 | 新工程、新组件、抽公共函数、换语言标准或整理目录 | 同样三步;先定 target 图与标准,再定模块与函数,最后跑完整检查 |
24
24
 
25
- 共同不变量:不发明项目没有声明的 target、标准、编译器或目录;不覆盖已有 `CMakeLists.txt`;不用全局 `include_directories()`、`add_definitions()`、`CMAKE_CXX_FLAGS` 给所有 target 加料;不写 `file(GLOB ...)`;不把 `main()` 编进库;库与程序不下载依赖,只有测试需要时调用 `LinkSettings_FetchGoogleTest`。后一步发现前一步没做完,退回那一步。
25
+ 两种走法共用这些约束:不发明项目没有声明的 target、标准、编译器或目录;不覆盖已有 `CMakeLists.txt`;不用全局 `include_directories()`、`add_definitions()`、`CMAKE_CXX_FLAGS` 给所有 target 加料;不写 `file(GLOB ...)`;不把 `main()` 编进库;库与程序不下载依赖,只有测试需要时调用 `LinkSettings_FetchGoogleTest`。后一步发现前一步没做完,退回那一步。
26
26
 
27
27
  ## 步骤
28
28
 
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  ## 适用场景
11
11
 
12
- 管 `.h` / `.hpp` / `.cpp` / `.cc` 里换一台机器也不会改的内容:可观察行为、库与程序的边界、声明、命名、异常和源码注释。文件槽位与守卫以 `references/file-template.md` 为准;排版、命名、头文件与包含顺序以 `references/google-cpp-style.md` 的 Google 基线为准。异常条文采用 C++ Core Guidelines 的 Error handling,作为开发阶段的源码指导;它不进入插件运行时。`CMakeLists.txt` 与 `cmake/` 开 `configuring-cmake`;`running-build.sh`、`.clang-*`、`.vscode/` 和 `environment.json` 开 `building-cpp`。只改函数体、命名、异常或注释且入口命令可运行时,定实现后只开 `building-cpp` 的「通过检查」;新增或移动源文件、改库与程序的边界、补测试 target 时,声明和实现可以先写,但完成前必须开 `configuring-cmake` 把文件写进 CMake,再开 `building-cpp` 定工具配置并通过检查。`.qmd`、仓库 README、Python 和非 C++ 文档不打开本技能。
12
+ 本技能处理换一台机器也不会改的源码内容:可观察行为、库与程序的边界、声明、命名、异常和源码注释。文件槽位与守卫以 `references/file-template.md` 为准;排版、命名、头文件与包含顺序以 `references/google-cpp-style.md` 的 Google 基线为准。异常条文取自 C++ Core Guidelines 的 Error handling,只约束开发期的源码,不进入插件运行时。`CMakeLists.txt` 与 `cmake/` 开 `configuring-cmake`;`running-build.sh`、`.clang-*`、`.vscode/` 和 `environment.json` 开 `building-cpp`。只改函数体、命名、异常或注释且入口命令可运行时,定实现后只开 `building-cpp` 的「通过检查」;新增或移动源文件、改库与程序的边界、补测试 target 时,声明和实现可以先写,但完成前必须开 `configuring-cmake` 把文件写进 CMake,再开 `building-cpp` 定工具配置并通过检查。`.qmd`、仓库 README、Python 和非 C++ 文档不打开本技能。
13
13
 
14
14
  存在项目技能 `cpp-workflow-project` 时先读它的 `SKILL.md`:工程名、库与程序的边界、阶段表、术语增补、入口命令。没有项目技能时门禁不会放出本技能,不要猜项目约定。
15
15
 
@@ -22,7 +22,7 @@ metadata:
22
22
  | 增量修正 | 用户点了一个函数、一条诊断或一个文件 | 沿本技能步骤走,只改判为不合格的那一处,其余保持原样 |
23
23
  | 整段重构 | 新工程、新阶段、用户没点名范围,或改动跨过库与程序的边界 | 同样四步;第 1、2 步先定删留,不动函数体;第 3、4 步才改代码 |
24
24
 
25
- 共同不变量:不发明阶段表和用户请求里没有的行为、类型和命令;只改注释或格式时不改可观察行为;不覆盖已有源码;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。后一步发现前一步没做完,退回那一步,不在当前步补结构。拿不准先用增量修正;做到一半发现边界被打破,退回整段重构,从被打破的那一步重走。
25
+ 两种走法共用这些约束:不发明阶段表和用户请求里没有的行为、类型和命令;只改注释或格式时不改可观察行为;不覆盖已有源码;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。后一步发现前一步没做完,退回那一步,不在当前步补结构。拿不准先用增量修正;做到一半发现边界被打破,退回整段重构,从被打破的那一步重走。
26
26
 
27
27
  ## 步骤
28
28
 
@@ -129,20 +129,19 @@ GameResult result_ = GameResult::kInProgress;
129
129
  ```
130
130
 
131
131
  - C++ 源文件用以上格式;CMake 用 `#`,YAML/JSON 用 `#`。
132
- - 超过约 200 行的文件用章节横幅分组,组名是读者要跳转的锚点;一个文件不超过 8 个横幅。
132
+ - 超过约 200 行的文件用空行分成几节,每节的说明写在一行 `///` 上,不画装饰线。
133
133
  - 文件头 3–7 行;`@file` 与 `@brief` 各占一行,后面只写本文件的范围与组成,不写实现步骤、作者和变更史。
134
134
 
135
- ## 排版:符号、宽度、横幅与标签
135
+ ## 排版:符号、宽度与标签
136
136
 
137
- - 行宽跟项目 `.clang-format` 的 `ColumnLimit` 走(Google 基线是 80 列,中文按两列计);横幅与列宽同宽,不另设数字。
138
- - 横幅宽度等于 `ColumnLimit`:`//===` 起头、`===//` 收尾,中间补 `-`;章节横幅同一格式。
137
+ - 行宽跟项目 `.clang-format` 的 `ColumnLimit` 走(Google 基线是 80 列,中文按两列计)。
139
138
  - 行尾注释与代码之间空两格;`///<` 只接在枚举项和 `struct` 短字段声明行尾。
140
139
  - `@param`、`@return`、`@throws` 标签保持英文;`@param` 后先写参数名,再写一句。
141
140
  - `TODO:` 后先写定位(阶段名或 issue 号),再写一个短横线,再写动作;不写 `TODO(某人)`,不写没有动作的 TODO。
142
141
  - `.clang-format`、`.clang-tidy`、`.clangd`、CMake 的配置项旁边写「为什么这样设」,不写「这一项是什么」;配置注释贴在被解释的键上方。
143
142
  - 禁止:大段表格、装饰图案、表情;外部资料路径、跨仓库引用、团队流程;注释掉的代码;注释元叙述(「其余内容见某处」「两处不重复」)。
144
- - `///`、`@param`、横幅不进入 `.qmd`、YAML、CSS。
145
- - C++ 源文件的说明性注释一律 `///`,实现区不与 `//` 混用;`//` 只留给文件头横幅边框与被引用的配置行。
143
+ - `///` 与 `@param` 不进入 `.qmd`、YAML、CSS。
144
+ - C++ 源文件的说明性注释一律 `///`,实现区不与 `//` 混用;`//` 只留给 `// NOLINTNEXTLINE(...)` 这类工具指令。
146
145
 
147
146
  ## 润色:删复述,统一句式
148
147
 
@@ -98,5 +98,5 @@ Google 原文不使用 C++ 异常;本工作流只在开发阶段的源码指
98
98
  ## 注释
99
99
 
100
100
  - 注释写意图、契约、取舍,不复述代码。
101
- - 文件头、章节横幅、声明注释、实现注释、配置注释的格式见 `references/comments.md`。
101
+ - 文件头、声明注释、实现注释与配置注释的格式见 `references/comments.md`。
102
102
  - TODO 统一写 `// TODO: <阶段或 issue> - <要做什么>`;不要写 `@todo`、`FIXME` 或只有「参考」没有动作的占位。
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // 只读注释审计:按 references/comments.md 的硬规则检查 C/C++ 源文件。
3
3
  //
4
- // 检查项:文件头横幅、横幅宽度、注释行宽度、Doxygen 标签、注释与声明的贴合、
4
+ // 检查项:Doxygen 文件头、注释行宽度、标签、注释与声明的贴合、
5
5
  // 类型注释存在性、成员功能清单、设计论证(方案对比)、否定式边界、领域尺寸、谓词首句、@param 完整性、枚举成员注释一致性、数据成员主注释位置。
6
6
  // 不重写任何文件。
7
7
  //
8
8
  // 用法:
9
- // node checking-comments.mjs [--root DIR] [--width N] [--banner-width N] [--no-header] [--json] [--quiet]
9
+ // node checking-comments.mjs [--root DIR] [--width N] [--no-header] [--json] [--quiet]
10
10
  //
11
11
  // 退出码:0 无问题,1 发现问题,2 用法或读取错误。
12
12
  import { readdirSync, readFileSync, statSync } from 'node:fs'
@@ -28,7 +28,6 @@ const SKIP_DIR = new Set([
28
28
  const STATIC_FORBIDDEN_TAGS = ['@returns', '@details', '@since', '@todo']
29
29
  const PRIVACY_TAGS = ['@author', '@date', '@copyright', '@version']
30
30
  const FORBIDDEN_TAGS = [...STATIC_FORBIDDEN_TAGS, ...PRIVACY_TAGS]
31
- const BANNER = /^\/\/=+-+=+\/\/$/
32
31
  const DOXY_FILE = /^\/\/\/\s*@file\s+\S/
33
32
  const DOXY_BRIEF = /^\/\/\/\s*@brief\s+\S/
34
33
  const MEMBER_LIST = /提供[^。]{0,40}、[^。]{0,40}(的功能|的能力|的接口)|功能[::]/
@@ -42,12 +41,12 @@ const DECL_KEYWORDS = /^(class|struct|union|enum|namespace|using|typedef|templat
42
41
 
43
42
  function usage(message) {
44
43
  if (message) process.stderr.write(`错误:${message}\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
+ process.stderr.write('用法:node checking-comments.mjs [--root DIR] [--width N] [--no-header] [--no-brief] [--allow-tag TAG] [--json] [--quiet]\n')
46
45
  return EXIT.usage
47
46
  }
48
47
 
49
48
  function parseArgs(argv) {
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
+ const options = { root: undefined, width: 80, widthExplicit: false, requireHeader: true, requireBrief: true, allowTags: new Set(), json: false, quiet: false }
51
50
  for (let index = 0; index < argv.length; index += 1) {
52
51
  const arg = argv[index]
53
52
  if (arg === '--root') {
@@ -57,10 +56,6 @@ function parseArgs(argv) {
57
56
  options.width = Number(argv[index + 1])
58
57
  options.widthExplicit = true
59
58
  index += 1
60
- } else if (arg === '--banner-width') {
61
- options.bannerWidth = Number(argv[index + 1])
62
- options.bannerWidthExplicit = true
63
- index += 1
64
59
  } else if (arg === '--no-header') {
65
60
  options.requireHeader = false
66
61
  } else if (arg === '--no-brief') {
@@ -80,10 +75,8 @@ function parseArgs(argv) {
80
75
  throw new Error(`未知参数 ${arg}`)
81
76
  }
82
77
  }
83
- for (const key of ['width', 'bannerWidth']) {
84
- if (!Number.isInteger(options[key]) || options[key] < 40) {
85
- throw new Error(`--${key} 需要不小于 40 的整数`)
86
- }
78
+ if (!Number.isInteger(options.width) || options.width < 40) {
79
+ throw new Error('--width 需要不小于 40 的整数')
87
80
  }
88
81
  return options
89
82
  }
@@ -208,7 +201,6 @@ function classMemberLines(lines) {
208
201
  /** 检查一段文本,返回问题列表;每个问题带 1 起始的行号。 */
209
202
  export function checkText(text, options = {}) {
210
203
  const width = options.width ?? 80
211
- const bannerWidth = options.bannerWidth ?? 80
212
204
  const requireHeader = options.requireHeader ?? true
213
205
  const lines = text.split(/\r?\n/)
214
206
  const issues = []
@@ -223,13 +215,12 @@ export function checkText(text, options = {}) {
223
215
  doxyHeaderEnd = firstContent
224
216
  while (doxyHeaderEnd + 1 < lines.length && lines[doxyHeaderEnd + 1].trim().startsWith('///')) doxyHeaderEnd += 1
225
217
  }
226
- if (requireHeader && firstContent !== -1 && !BANNER.test(firstTrimmed) && !hasDoxyHeader) {
218
+ if (requireHeader && firstContent !== -1 && !hasDoxyHeader) {
227
219
  add(firstContent + 1, 'header-missing', '文件开头缺少文件头:写 /// @file <文件名>,下一行写 /// @brief')
228
220
  }
229
- if (firstContent !== -1 && (BANNER.test(firstTrimmed) || hasDoxyHeader)) {
221
+ if (firstContent !== -1 && hasDoxyHeader) {
230
222
  const own = options.fileName ? options.fileName.split(/[\\/]/).pop() : ''
231
- const bannerHeader = BANNER.test(firstTrimmed)
232
- if (hasDoxyHeader && own) {
223
+ if (own) {
233
224
  const declared = /@file\s+(\S+)/.exec(lines[firstContent].trim())
234
225
  if (declared && declared[1] !== own) {
235
226
  add(firstContent + 1, 'header-file-name', `文件头写的是 ${declared[1]},本文件是 ${own}:@file 必须等于文件名`)
@@ -240,7 +231,7 @@ export function checkText(text, options = {}) {
240
231
  }
241
232
  for (let index = firstContent + 1; index < lines.length; index += 1) {
242
233
  const trimmed = lines[index].trim()
243
- if (bannerHeader ? BANNER.test(trimmed) || !trimmed.startsWith('//') : !trimmed.startsWith('///')) break
234
+ if (!trimmed.startsWith('///')) break
244
235
  const names = trimmed.match(/\b[A-Za-z0-9_-]+\.(?:h|hpp|cpp|cc)\b/g) ?? []
245
236
  const fileMatches = trimmed.match(/@file\s+(\S+)/)
246
237
  const namesToCheck = fileMatches ? names.filter((name) => name !== fileMatches[1]) : names
@@ -254,11 +245,6 @@ export function checkText(text, options = {}) {
254
245
  for (let index = 0; index < lines.length; index += 1) {
255
246
  const line = lines[index]
256
247
  const trimmed = line.trim()
257
- if (BANNER.test(trimmed)) {
258
- const actual = displayWidth(trimmed)
259
- if (actual !== bannerWidth) add(index + 1, 'banner-width', `横幅宽度 ${actual} 列,应为 ${bannerWidth} 列`)
260
- continue
261
- }
262
248
  if (trimmed.startsWith('///') || (trimmed.startsWith('//') && !trimmed.startsWith('//!'))) {
263
249
  if (displayWidth(trimmed) > width) add(index + 1, 'comment-width', `注释行 ${displayWidth(trimmed)} 列,超过 ${width} 列`)
264
250
  for (const tag of FORBIDDEN_TAGS) {
@@ -453,7 +439,6 @@ function main(argv) {
453
439
  const columnLimit = readColumnLimit(root)
454
440
  if (columnLimit !== undefined) {
455
441
  if (!options.widthExplicit) options.width = columnLimit
456
- if (!options.bannerWidthExplicit) options.bannerWidth = columnLimit
457
442
  }
458
443
  let files
459
444
  try {
@@ -24,7 +24,7 @@
24
24
 
25
25
  **异常声明**:构造函数、`Get` 类查询、解析函数最容易抛;在声明注释里写「当……时抛出 `std::out_of_range`」这样的具体类型,不要只写「可能抛异常」,也不要给每个参数错误都新建类型。类型选择见 `references/exceptions.md`。
26
26
 
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`。
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
 
@@ -9,7 +9,7 @@
9
9
  - 函数保持短小;一个函数只做一件事。超过约 40 行时先找可以命名的中间步骤,不要用注释把长函数切成几段。
10
10
  - 按「准备 → 操作 → 收尾」排列语句,用空行分组;组与组之间的空行表达阶段,不用大段叙述。
11
11
  - `.cpp` 的第一个 include 是自己的配对头,然后直接包含本文件用到的标准库头;不要依赖公共头替本文件转发声明。包含顺序见 `references/google-cpp-style.md`;文件槽位见 `references/file-template.md`。
12
- - `.cpp` / `.cc` 里,重要函数按空行分阶段,每个阶段上方写一句 `//`:写边界、失败后果或为什么停在这里,不把下一行的条件翻译一遍。`IsWin` 这种单行函数不配注释;只写删掉会让下一次修改得到错误结果、而代码本身看不出的内容,变更史留给提交说明。
12
+ - `.cpp` / `.cc` 里,重要函数按空行分阶段,每个阶段上方写一句 `///`:写边界、失败后果或为什么停在这里,不把下一行的条件翻译一遍。`IsWin` 这种单行函数不配注释;只写删掉会让下一次修改得到错误结果、而代码本身看不出的内容,变更史留给提交说明。
13
13
  - 错误处理:失败是调用方正常结果时返回结果类型、`bool` 或 `std::optional`;函数无法完成自己的任务时抛异常,不拿异常做正常分支。Debug 与 Release 用同一套返回值与抛出条件,只有诊断详略不同。两种模式都要守的 invariant 用 `Check`(项目保留时),只在开发期做的自检用 `DebugCheck`;没有这两个宏就分别用 `assert` 与 `std::terminate`。捕获时按 `const` 引用,只处理能处理的类型,其余继续抛。消息按 `Type::Function: 条件 (值); 限度` 组织,见 `references/exceptions.md` 的「消息」节。
14
14
  - 声明 `noexcept` 前核对函数体:出现 `at()` 或其他可能抛出的调用就去掉。越界用 `at()` 抛 `std::out_of_range`,这个函数就不能标 `noexcept`。
15
15
  - 组合输出用 `std::format`,再写入 `std::cout` / `std::cerr`;不要在库实现里直接打印。C++23 且库支持时程序边界可用 `std::print`,C++20 及更早没有。